 
  /*--------------------------------------:noTabs=true:tabSize=2:indentSize=2:--
    --  Xinha (is not htmlArea) - http://xinha.org
    --
    --  Use of Xinha is granted by the terms of the htmlArea License (based on
    --  BSD license)  please read license.txt in this package for details.
    --
    --  Copyright (c) 2005-2008 Xinha Developer Team and contributors
    --  
    --  Xinha was originally based on work by Mihai Bazon which is:
    --      Copyright (c) 2003-2004 dynarch.com.
    --      Copyright (c) 2002-2003 interactivetools.com, inc.
    --      This copyright notice MUST stay intact for use.
    --
    --  Developers - Coding Style: 
    --         Before you are going to work on Xinha code, please see http://trac.xinha.org/wiki/Documentation/StyleGuide
    --
    --  $HeadURL: http://svn.xinha.org/trunk/XinhaCore.js $
    --  $LastChangedDate: 2012-06-22 13:56:59 +0000 (Fri, 22 Jun 2012) $
    --  $LastChangedRevision: 1326 $
    --  $LastChangedBy: gogo $
    --------------------------------------------------------------------------*/
/*jslint regexp: false, rhino: false, browser: true, bitwise: false, forin: true, adsafe: false, evil: true, nomen: false, 
glovar: false, debug: false, eqeqeq: false, passfail: false, sidebar: false, laxbreak: false, on: false, cap: true, 
white: false, widget: false, undef: true, plusplus: false*/
/*global  Dialog , _editor_css , _editor_icons, _editor_lang , _editor_skin , _editor_url, dumpValues, ActiveXObject, HTMLArea, _editor_lcbackend*/

/** Information about the Xinha version 
 * @type Object
 */
Xinha.version =
{
  'Release'   : 'Trunk',
  'Head'      : '$HeadURL: http://svn.xinha.org/trunk/XinhaCore.js $'.replace(/^[^:]*:\s*(.*)\s*\$$/, '$1'),
  'Date'      : '$LastChangedDate: 2012-06-22 13:56:59 +0000 (Fri, 22 Jun 2012) $'.replace(/^[^:]*:\s*([0-9\-]*) ([0-9:]*) ([+0-9]*) \((.*)\)\s*\$/, '$4 $2 $3'),
  'Revision'  : '$LastChangedRevision: 1326 $'.replace(/^[^:]*:\s*(.*)\s*\$$/, '$1'),
  'RevisionBy': '$LastChangedBy: gogo $'.replace(/^[^:]*:\s*(.*)\s*\$$/, '$1')
};

//must be here. it is called while converting _editor_url to absolute
Xinha._resolveRelativeUrl = function( base, url )
{
  if(url.match(/^([^:]+\:)?\/\//))
  {
    return url;
  }
  else
  {
    var b = base.split("/");
    if(b[b.length - 1] === "")
    {
      b.pop();
    }
    var p = url.split("/");
    if(p[0] == ".")
    {
      p.shift();
    }
    while(p[0] == "..")
    {
      b.pop();
      p.shift();
    }
    return b.join("/") + "/" + p.join("/");
  }
};

if ( typeof _editor_url == "string" )
{
  // Leave exactly one backslash at the end of _editor_url
  _editor_url = _editor_url.replace(/\x2f*$/, '/');
  
  // convert _editor_url to absolute
  if(!_editor_url.match(/^([^:]+\:)?\//))
  {
    (function()
    {
      var tmpPath = window.location.toString().replace(/\?.*$/,'').split("/");
      tmpPath.pop();
      _editor_url = Xinha._resolveRelativeUrl(tmpPath.join("/"), _editor_url);
    })();
  }
}
else
{
  alert("WARNING: _editor_url is not set!  You should set this variable to the editor files path; it should preferably be an absolute path, like in '/xinha/', but it can be relative if you prefer.  Further we will try to load the editor files correctly but we'll probably fail.");
  _editor_url = '';
}

// make sure we have a language
if ( typeof _editor_lang == "string" )
{
  _editor_lang = _editor_lang.toLowerCase();
}
else
{
  _editor_lang = "en";
}

// skin stylesheet to load
if ( typeof _editor_skin !== "string" )
{
  _editor_skin = "";
}

if ( typeof _editor_icons !== "string" )
{
  _editor_icons = "";
}
/**
* The list of Xinha editors on the page. May be multiple editors.
* You can access each editor object through this global variable.
*
* Example:<br />
* <code>
*	var html = __xinhas[0].getEditorContent(); // gives you the HTML of the first editor in the page
* </code>
*/
var __xinhas = [];

// browser identification
/** Cache the user agent for the following checks
 * @type String
 * @private
 */
Xinha.agt       = navigator.userAgent.toLowerCase();
/** Browser is Microsoft Internet Explorer
 * @type Boolean 
 */
Xinha.is_ie    = ((Xinha.agt.indexOf("msie") != -1) && (Xinha.agt.indexOf("opera") == -1));
/** Version Number, if browser is Microsoft Internet Explorer
 * @type Float 
 */
Xinha.ie_version= parseFloat(Xinha.agt.substring(Xinha.agt.indexOf("msie")+5));
/** Browser is Opera
 * @type Boolean 
 */
Xinha.is_opera  = (Xinha.agt.indexOf("opera") != -1);
/** Version Number, if browser is Opera 
  * @type Float 
  */
if(Xinha.is_opera && Xinha.agt.match(/opera[\/ ]([0-9.]+)/))
{
  Xinha.opera_version = parseFloat(RegExp.$1);
}
else
{
  Xinha.opera_version = 0;
}
/** Browserengine is KHTML (Konqueror, Safari)
 * @type Boolean 
 */
Xinha.is_khtml  = (Xinha.agt.indexOf("khtml") != -1);
/** Browser is WebKit
 * @type Boolean 
 */
Xinha.is_webkit  = (Xinha.agt.indexOf("applewebkit") != -1);
/** Webkit build number
 * @type Integer
 */
Xinha.webkit_version = parseInt(navigator.appVersion.replace(/.*?AppleWebKit\/([\d]).*?/,'$1'), 10);

/** Browser is Safari
 * @type Boolean 
 */
Xinha.is_safari  = (Xinha.agt.indexOf("safari") != -1);

/** Browser is Google Chrome
 * @type Boolean 
 */
Xinha.is_chrome = (Xinha.agt.indexOf("chrome") != -1);

/** OS is MacOS
 * @type Boolean 
 */
Xinha.is_mac	   = (Xinha.agt.indexOf("mac") != -1);
/** Browser is Microsoft Internet Explorer Mac
 * @type Boolean 
 */
Xinha.is_mac_ie = (Xinha.is_ie && Xinha.is_mac);
/** Browser is Microsoft Internet Explorer Windows
 * @type Boolean 
 */
Xinha.is_win_ie = (Xinha.is_ie && !Xinha.is_mac);
/** Browser engine is Gecko (Mozilla), applies also to Safari and Opera which work
 *  largely similar.
 *@type Boolean 
 */
Xinha.is_gecko  = (navigator.product == "Gecko") || Xinha.is_opera;
/** Browser engine is really Gecko, i.e. Browser is Firefox (or Netscape, SeaMonkey, Flock, Songbird, Beonex, K-Meleon, Camino, Galeon, Kazehakase, Skipstone, or whatever derivate might exist out there...)
 * @type Boolean 
 */
Xinha.is_real_gecko = (navigator.product == "Gecko" && !Xinha.is_webkit);

/** Gecko version lower than 1.9
 * @type Boolean 
 */
Xinha.is_ff2 = Xinha.is_real_gecko && parseInt(navigator.productSub.substr(0,10), 10) < 20071210;

/** File is opened locally opened ("file://" protocol)
 * @type Boolean
 * @private
 */
Xinha.isRunLocally = document.URL.toLowerCase().search(/^file:/) != -1;
/** Editing is enabled by document.designMode (Gecko, Opera), as opposed to contenteditable (IE)
 * @type Boolean
 * @private
 */
Xinha.is_designMode = (typeof document.designMode != 'undefined' && !Xinha.is_ie); // IE has designMode, but we're not using it

/** Check if Xinha can run in the used browser, otherwise the textarea will be remain unchanged
 * @type Boolean
 * @private
 */
Xinha.checkSupportedBrowser = function()
{
  return Xinha.is_real_gecko || (Xinha.is_opera && Xinha.opera_version >= 9.2) || Xinha.ie_version >= 5.5 || Xinha.webkit_version >= 522;
};
/** Cache result of checking for browser support
 * @type Boolean
 * @private
 */
Xinha.isSupportedBrowser = Xinha.checkSupportedBrowser();

if ( Xinha.isRunLocally && Xinha.isSupportedBrowser)
{
  alert('Xinha *must* be installed on a web server. Locally opened files (those that use the "file://" protocol) cannot properly function. Xinha will try to initialize but may not be correctly loaded.');
}

/** Creates a new Xinha object
 * @version $Rev: 1326 $ $LastChangedDate: 2012-06-22 13:56:59 +0000 (Fri, 22 Jun 2012) $
 * @constructor
 * @param {String|DomNode}   textarea the textarea to replace; can be either only the id or the DOM object as returned by document.getElementById()
 * @param {Xinha.Config} config optional if no Xinha.Config object is passed, the default config is used
 */
function Xinha(textarea, config)
{ 
  if ( !Xinha.isSupportedBrowser )
  {
    return;
  }
  
  if ( !textarea )
  {
    throw new Error ("Tried to create Xinha without textarea specified.");
  }

  if ( typeof config == "undefined" )
  {
		/** The configuration used in the editor
		 * @type Xinha.Config
		 */
    this.config = new Xinha.Config();
  }
  else
  {
    this.config = config;
  }

  if ( typeof textarea != 'object' )
  {
    textarea = Xinha.getElementById('textarea', textarea);
  }
  /** This property references the original textarea, which is at the same time the editor in text mode
   * @type DomNode textarea
   */
  this._textArea = textarea;
  this._textArea.spellcheck = false;
  Xinha.freeLater(this, '_textArea');
  
  // 
  /** Before we modify anything, get the initial textarea size
   * @private
   * @type Object w,h 
   */
  this._initial_ta_size =
  {
    w: textarea.style.width  ? textarea.style.width  : ( textarea.offsetWidth  ? ( textarea.offsetWidth  + 'px' ) : ( textarea.cols + 'em') ),
    h: textarea.style.height ? textarea.style.height : ( textarea.offsetHeight ? ( textarea.offsetHeight + 'px' ) : ( textarea.rows + 'em') )
  };

  if ( document.getElementById("loading_" + textarea.id) || this.config.showLoading )
  {
    if (!document.getElementById("loading_" + textarea.id))
    {
      Xinha.createLoadingMessage(textarea);
    }
    this.setLoadingMessage(Xinha._lc("Constructing object"));
  }

  /** the current editing mode
  * @private 
  * @type string "wysiwyg"|"text"
  */
  this._editMode = "wysiwyg";
  /** this object holds the plugins used in the editor
  * @private 
  * @type Object
  */
  this.plugins = {};
  /** periodically updates the toolbar
  * @private 
  * @type timeout
  */
  this._timerToolbar = null;
  /** periodically takes a snapshot of the current editor content
  * @private 
  * @type timeout
  */
  this._timerUndo = null;
  /** holds the undo snapshots
  * @private 
  * @type Array
  */
  this._undoQueue = [this.config.undoSteps];
  /** the current position in the undo queue 
  * @private 
  * @type integer
  */
  this._undoPos = -1;
  /** use our own undo implementation (true) or the browser's (false) 
  * @private 
  * @type Boolean
  */
  this._customUndo = true;
  /** the document object of the page Xinha is embedded in
  * @private 
  * @type document
  */
  this._mdoc = document; // cache the document, we need it in plugins
  /** doctype of the edited document (fullpage mode)
  * @private 
  * @type string
  */
  this.doctype = '';
  /** running number that identifies the current editor
  * @public 
  * @type integer
  */
  this.__htmlarea_id_num = __xinhas.length;
  __xinhas[this.__htmlarea_id_num] = this;
	
  /** holds the events for use with the notifyOn/notifyOf system
  * @private 
  * @type Object
  */
  this._notifyListeners = {};

  // Panels
  var panels = 
  {
    right:
    {
      on: true,
      container: document.createElement('td'),
      panels: []
    },
    left:
    {
      on: true,
      container: document.createElement('td'),
      panels: []
    },
    top:
    {
      on: true,
      container: document.createElement('td'),
      panels: []
    },
    bottom:
    {
      on: true,
      container: document.createElement('td'),
      panels: []
    }
  };

  for ( var i in panels )
  {
    if(!panels[i].container) { continue; } // prevent iterating over wrong type
    panels[i].div = panels[i].container; // legacy
    panels[i].container.className = 'panels panels_' + i;
    Xinha.freeLater(panels[i], 'container');
    Xinha.freeLater(panels[i], 'div');
  }
  /** holds the panels
  * @private 
  * @type Array
  */
  // finally store the variable
  this._panels = panels;
	
  // Init some properties that are defined later
  /** The statusbar container
   * @type DomNode statusbar div
   */
  this._statusBar = null;
  /** The DOM path that is shown in the statusbar in wysiwyg mode
   * @private
   * @type DomNode
   */
  this._statusBarTree = null;
  /** The message that is shown in the statusbar in text mode
   * @private
   * @type DomNode
   */
  this._statusBarTextMode = null;
  /** Holds the items of the DOM path that is shown in the statusbar in wysiwyg mode
   * @private
   * @type Array tag names
   */
  this._statusBarItems = [];
  /** Holds the parts (table cells) of the UI (toolbar, panels, statusbar)

   * @type Object framework parts
   */
  this._framework = {};
  /** Them whole thing (table)
   * @private
   * @type DomNode
   */
  this._htmlArea = null;
  /** This is the actual editable area.<br />
   *  Technically it's an iframe that's made editable using window.designMode = 'on', respectively document.body.contentEditable = true (IE).<br />
   *  Use this property to get a grip on the iframe's window features<br />
   *
   * @type window
   */
  this._iframe = null;
  /** The document object of the iframe.<br />
  *   Use this property to perform DOM operations on the edited document
  * @type document
  */
  this._doc = null;
  /** The toolbar
   *  @private
   *  @type DomNode 
   */
  this._toolBar = this._toolbar = null; //._toolbar is for legacy, ._toolBar is better thanks.
  /** Holds the botton objects
   *  @private
   *  @type Object
   */
  this._toolbarObjects = {};
  
  //hook in config.Events as as a "plugin"
  this.plugins.Events = 
  {
    name: 'Events',
    developer : 'The Xinha Core Developer Team',
    instance: config.Events
  };
};
// ray: What is this for? Do we need it?
Xinha.onload = function() { };
Xinha.init = function() { Xinha.onload(); };

// cache some regexps
/** Identifies HTML tag names
* @type RegExp
*/
Xinha.RE_tagName  = /(<\/|<)\s*([^ \t\n>]+)/ig;
/** Exracts DOCTYPE string from HTML
* @type RegExp
*/
Xinha.RE_doctype  = /(<!doctype((.|\n)*?)>)\n?/i;
/** Finds head section in HTML
* @type RegExp
*/
Xinha.RE_head     = /<head>((.|\n)*?)<\/head>/i;
/** Finds body section in HTML
* @type RegExp
*/
Xinha.RE_body     = /<body[^>]*>((.|\n|\r|\t)*?)<\/body>/i;
/** Special characters that need to be escaped when dynamically creating a RegExp from an arbtrary string
* @private
* @type RegExp
*/
Xinha.RE_Specials = /([\/\^$*+?.()|{}\[\]])/g;
/** When dynamically creating a RegExp from an arbtrary string, some charactes that have special meanings in regular expressions have to be escaped.
*   Run any string through this function to escape reserved characters.
* @param {string} string the string to be escaped
* @returns string
*/
Xinha.escapeStringForRegExp = function (string)
{
  return string.replace(Xinha.RE_Specials, '\\$1');
};
/** Identifies email addresses
* @type RegExp
*/
Xinha.RE_email    = /^[_a-z\d\-\.]{3,}@[_a-z\d\-]{2,}(\.[_a-z\d\-]{2,})+$/i;
/** Identifies URLs
* @type RegExp
*/
Xinha.RE_url      = /(https?:\/\/)?(([a-z0-9_]+:[a-z0-9_]+@)?[a-z0-9_\-]{2,}(\.[a-z0-9_\-]{2,}){2,}(:[0-9]+)?(\/\S+)*)/i;



/**
 * This class creates an object that can be passed to the Xinha constructor as a parameter.
 * Set the object's properties as you need to configure the editor (toolbar etc.)
 * @version $Rev: 1326 $ $LastChangedDate: 2012-06-22 13:56:59 +0000 (Fri, 22 Jun 2012) $
 * @constructor
 */
Xinha.Config = function()
{
  /** The svn revision number 
   * @type Number
   */
  this.version = Xinha.version.Revision;
  
 /** This property controls the width of the editor.<br />
  *  Allowed values are 'auto', 'toolbar' or a numeric value followed by "px".<br />
  *  <code>auto</code>: let Xinha choose the width to use.<br />
  *  <code>toolbar</code>: compute the width size from the toolbar width.<br />
  *  <code>numeric value</code>: forced width in pixels ('600px').<br />
  * 
  *  Default: <code>"auto"</code>
  * @type String
  */
  this.width  = "auto";
 /** This property controls the height of the editor.<br />
  *  Allowed values are 'auto' or a numeric value followed by px.<br />
  *  <code>"auto"</code>: let Xinha choose the height to use.<br />
  *  <code>numeric value</code>: forced height in pixels ('200px').<br />
  *  Default: <code>"auto"</code> 
  * @type String
  */
  this.height = "auto";

 /** Specifies whether the toolbar should be included
  *  in the size, or are extra to it.  If false then it's recommended
  *  to have the size set as explicit pixel sizes (either in Xinha.Config or on your textarea)<br />
  *
  *  Default: <code>true</code>
  *
  *  @type Boolean
  */
  this.sizeIncludesBars = true;
 /**
  * Specifies whether the panels should be included
  * in the size, or are extra to it.  If false then it's recommended
  * to have the size set as explicit pixel sizes (either in Xinha.Config or on your textarea)<br />
  *  
  *  Default: <code>true</code>
  *
  *  @type Boolean
  */
  this.sizeIncludesPanels = true;

 /**
  * each of the panels has a dimension, for the left/right it's the width
  * for the top/bottom it's the height.
  *
  * WARNING: PANEL DIMENSIONS MUST BE SPECIFIED AS PIXEL WIDTHS<br />
  *Default values:  
  *<pre>
  *	  xinha_config.panel_dimensions =
  *   {
  *	    left:   '200px', // Width
  *	    right:  '200px',
  *	    top:    '100px', // Height
  *	    bottom: '100px'
  *	  }
  *</pre>
  *  @type Object
  */
  this.panel_dimensions =
  {
    left:   '200px', // Width
    right:  '200px',
    top:    '100px', // Height
    bottom: '100px'
  };

 /**  To make the iframe width narrower than the toolbar width, e.g. to maintain
  *   the layout when editing a narrow column of text, set the next parameter (in pixels).<br />
  *
  *  Default: <code>true</code>
  *
  *  @type Integer|null
  */
  this.iframeWidth = null;
 
 /** Enable creation of the status bar?<br />
  *
  *  Default: <code>true</code>
  *
  *  @type Boolean 
  */
  this.statusBar = true;

 /** Intercept ^V and use the Xinha paste command
  *  If false, then passes ^V through to browser editor widget, which is the only way it works without problems in Mozilla<br />
  *
  *  Default: <code>false</code>
  *
  *  @type Boolean
  */
  this.htmlareaPaste = false;
  
 /** <strong>Gecko only:</strong> Let the built-in routine for handling the <em>return</em> key decide if to enter <em>br</em> or <em>p</em> tags,
  *  or use a custom implementation.<br />
  *  For information about the rules applied by Gecko, <a href="http://www.mozilla.org/editor/rules.html">see Mozilla website</a> <br />
  *  Possible values are <em>built-in</em> or <em>best</em><br />
  *
  *  Default: <code>"best"</code>
  *
  *  @type String
  */
  this.mozParaHandler = 'best'; 
  
 /** This determines the method how the HTML output is generated.
  *  There are two choices:
  * 
  *<table border="1">
  *   <tr>
  *       <td><em>DOMwalk</em></td>
  *       <td>This is the classic and proven method. It recusively traverses the DOM tree 
  *           and builds the HTML string "from scratch". Tends to be a bit slow, especially in IE.</td>
  *   </tr>
  *   <tr>
  *       <td><em>TransformInnerHTML</em></td>
  *       <td>This method uses the JavaScript innerHTML property and relies on Regular Expressions to produce
  *            clean XHTML output. This method is much faster than the other one.</td>
  *     </tr>
  * </table>
  *
  *  Default: <code>"DOMwalk"</code>
  *
  * @type String
  */
  this.getHtmlMethod = 'DOMwalk';
  
  /** Maximum size of the undo queue<br />
   *  Default: <code>20</code>
   *  @type Integer
   */
  this.undoSteps = 20;

  /** The time interval at which undo samples are taken<br />
   *  Default: <code>500</code> (1/2 sec)
   *  @type Integer milliseconds
   */
  this.undoTimeout = 500;

  /** Set this to true if you want to explicitly right-justify when setting the text direction to right-to-left<br />
   *  Default: <code>false</code>
   *  @type Boolean
   */
  this.changeJustifyWithDirection = false;

  /** If true then Xinha will retrieve the full HTML, starting with the &lt;HTML&gt; tag.<br />
   *  Default: <code>false</code>
   *  @type Boolean
   */
  this.fullPage = false;

  /** Raw style definitions included in the edited document<br />
   *  When a lot of inline style is used, perhaps it is wiser to use one or more external stylesheets.<br />
   *  To set tags P in red, H1 in blue andn A not underlined, we may do the following
   *<pre>
   * xinha_config.pageStyle =
   *  'p { color:red; }\n' +
   *  'h1 { color:bleu; }\n' +
   *  'a {text-decoration:none; }';
   *</pre>
   *  Default: <code>""</code> (empty)
   *  @type String
   */
  this.pageStyle = "";

  /** Array of external stylesheets to load. (Reference these absolutely)<br />
   *  Example<br />
   *  <pre>xinha_config.pageStyleSheets = ["/css/myPagesStyleSheet.css","/css/anotherOne.css"];</pre>
   *  Default: <code>[]</code> (empty)
   *  @type Array
   */
  this.pageStyleSheets = [];

  // specify a base href for relative links
  /** Specify a base href for relative links<br />
   *  ATTENTION: this does not work as expected and needs t be changed, see Ticket #961 <br />
   *  Default: <code>null</code>
   *  @type String|null
   */
  this.baseHref = null;

  /** If true, relative URLs (../) will be made absolute. 
   *  When the editor is in different directory depth 
   *  as the edited page relative image sources will break the display of your images.
   *  this fixes an issue where Mozilla converts the urls of images and links that are on the same server 
   *  to relative ones (../) when dragging them around in the editor (Ticket #448)<br />
   *  Default: <code>true</code>
   *  @type Boolean
   */
  this.expandRelativeUrl = true;
  
 /**  We can strip the server part out of URL to make/leave them semi-absolute, reason for this
   *  is that the browsers will prefix  the server to any relative links to make them absolute, 
   *  which isn't what you want most the time.<br />
   *  Default: <code>true</code>
   *  @type Boolean
   */
  this.stripBaseHref = true;

   /**  We can strip the url of the editor page from named links (eg &lt;a href="#top"&gt;...&lt;/a&gt;) and links 
   *  that consist only of URL parameters (eg &lt;a href="?parameter=value"&gt;...&lt;/a&gt;)
   *  reason for this is that browsers tend to prefixe location.href to any href that
   *  that don't have a full url<br />
   *  Default: <code>true</code>
   *  @type Boolean
   */
  this.stripSelfNamedAnchors = true;

  /** In URLs all characters above ASCII value 127 have to be encoded using % codes<br />
   *  Default: <code>true</code>
   *  @type Boolean
   */
  this.only7BitPrintablesInURLs = true;

 
  /** If you are putting the HTML written in Xinha into an email you might want it to be 7-bit
   *  characters only.  This config option will convert all characters consuming
   *  more than 7bits into UNICODE decimal entity references (actually it will convert anything
   *  below <space> (chr 20) except cr, lf and tab and above <tilde> (~, chr 7E))<br />
   *  Default: <code>false</code>
   *  @type Boolean
   */
  this.sevenBitClean = false;


  /** Sometimes we want to be able to replace some string in the html coming in and going out
   *  so that in the editor we use the "internal" string, and outside and in the source view
   *  we use the "external" string  this is useful for say making special codes for
   *  your absolute links, your external string might be some special code, say "{server_url}"
   *  an you say that the internal represenattion of that should be http://your.server/<br />
   *  Example:  <code>{ 'html_string' : 'wysiwyg_string' }</code><br />
   *  Default: <code>{}</code> (empty)
   *  @type Object
   */
  this.specialReplacements = {}; //{ 'html_string' : 'wysiwyg_string' }
  
  /** A filter function for the HTML used inside the editor<br />
   * Default: function (html) { return html }
   * 
   * @param {String} html The whole document's HTML content
   * @return {String} The processed HTML 
   */
  this.inwardHtml = function (html) { return html; };
  
  /** A filter function for the generated HTML<br />
   * Default: function (html) { return html }
   * 
   * @param {String} html The whole document's HTML content
   * @return {String} The processed HTML 
   */
  this.outwardHtml = function (html) { return html; };
  
  /** This setting determines whether or not the editor will be automatically activated and focused when the page loads. 
   *  If the page contains only a single editor, autofocus can be set to true to focus it. 
   *  Alternatively, if the page contains multiple editors, autofocus may be set to the ID of the text area of the editor to be focused. 
   *  For example, the following setting would focus the editor attached to the text area whose ID is "myTextArea": 
   *  <code>xinha_config.autofocus = "myTextArea";</code>
   *  Default: <code>false</code>
   *  @type Boolean|String
   */
  this.autofocus = false;
  
 /** Set to true if you want Word code to be cleaned upon Paste. This only works if 
   * you use the toolbr button to paste, not ^V. This means that due to the restrictions
   * regarding pasting, this actually has no real effect in Mozilla <br />
   *  Default: <code>true</code>
   *  @type Boolean
   */
  this.killWordOnPaste = true;

  /** Enable the 'Target' field in the Make Link dialog. Note that the target attribute is invalid in (X)HTML strict<br />
   *  Default: <code>true</code>
   *  @type Boolean
   */
  this.makeLinkShowsTarget = true;

  /** CharSet of the iframe, default is the charset of the document
   *  @type String
   */
  this.charSet = (typeof document.characterSet != 'undefined') ? document.characterSet : document.charset;

 /** Whether the edited document should be rendered in Quirksmode or Standard Compliant (Strict) Mode.<br />
   * This is commonly known as the "doctype switch"<br />
   * for details read here http://www.quirksmode.org/css/quirksmode.html
   *
   * Possible values:<br />
   *    true     :  Quirksmode is used<br />
   *    false    :  Strict mode is used<br />
   *    null (default):  the mode of the document Xinha is in is used
   * @type Boolean|null
   */
  this.browserQuirksMode = null;

  // URL-s
  this.imgURL = "images/";
  this.popupURL = "popups/";

  /** RegExp allowing to remove certain HTML tags when rendering the HTML.<br />
   *  Example: remove span and font tags
   *  <code>
   *    xinha_config.htmlRemoveTags = /span|font/;
   *  </code>
   *  Default: <code>null</code>
   *  @type RegExp|null
   */
  this.htmlRemoveTags = null;

 /** Turning this on will turn all "linebreak" and "separator" items in your toolbar into soft-breaks,
   * this means that if the items between that item and the next linebreak/separator can
   * fit on the same line as that which came before then they will, otherwise they will
   * float down to the next line.

   * If you put a linebreak and separator next to each other, only the separator will
   * take effect, this allows you to have one toolbar that works for both flowToolbars = true and false
   * infact the toolbar below has been designed in this way, if flowToolbars is false then it will
   * create explictly two lines (plus any others made by plugins) breaking at justifyleft, however if
   * flowToolbars is false and your window is narrow enough then it will create more than one line
   * even neater, if you resize the window the toolbars will reflow.  <br />
   *  Default: <code>true</code>
   *  @type Boolean
   */
  this.flowToolbars = true;
  
  /** Set to center or right to change button alignment in toolbar
   *  @type String
   */
  this.toolbarAlign = "left";
  
  /** Set to true to display the font names in the toolbar font select list in their actual font.
   *  Note that this doesn't work in IE, but doesn't hurt anything either.
   *  Default: <code>false</code>
   *  @type Boolean
   */
   this.showFontStylesInToolbar = false;
  
  /** Set to true if you want the loading panel to show at startup<br />
   *  Default: <code>false</code>
   *  @type Boolean
   */
  this.showLoading = false;
  
  /** Set to false if you want to allow JavaScript in the content, otherwise &lt;script&gt; tags are stripped out.<br />
   *  This currently only affects the "DOMwalk" getHtmlMethod.<br />
   *  Default: <code>true</code>
   *  @type Boolean
   */
  this.stripScripts = true;

 /** See if the text just typed looks like a URL, or email address
   * and link it appropriatly
   * Note: Setting this option to false only affects Mozilla based browsers.
   * In InternetExplorer this is native behaviour and cannot be turned off.<br />
   *  Default: <code>true</code>
   *  @type Boolean
   */
   this.convertUrlsToLinks = true;

 /** Set to true to hide media objects when a div-type dialog box is open, to prevent show-through
  *  Default: <code>false</code>
  *  @type Boolean
  */
  this.hideObjectsBehindDialogs = false;

 /** Size of color picker cells<br />
   * Use number + "px"<br />
   *  Default: <code>"6px"</code>
   *  @type String
   */
  this.colorPickerCellSize = '6px';
 /** Granularity of color picker cells (number per column/row)<br />
   *  Default: <code>18</code>
   *  @type Integer
   */
  this.colorPickerGranularity = 18;
 /** Position of color picker from toolbar button<br />
   *  Default: <code>"bottom,right"</code>
   *  @type String
   */
  this.colorPickerPosition = 'bottom,right';
  /** Set to true to show only websafe checkbox in picker<br />
   *  Default: <code>false</code>
   *  @type Boolean
   */
  this.colorPickerWebSafe = false;
 /** Number of recent colors to remember<br />
   *  Default: <code>20</code>
   *  @type Integer
   */
  this.colorPickerSaveColors = 20;

  /** Start up the editor in fullscreen mode<br />
   *  Default: <code>false</code>
   *  @type Boolean
   */
  this.fullScreen = false;
  
 /** You can tell the fullscreen mode to leave certain margins on each side.<br />
   *  The value is an array with the values for <code>[top,right,bottom,left]</code> in that order<br />
   *  Default: <code>[0,0,0,0]</code>
   *  @type Array
   */
  this.fullScreenMargins = [0,0,0,0];
  
  
  /** Specify the method that is being used to calculate the editor's size<br/>
    * when we return from fullscreen mode.
    *  There are two choices:
    * 
    * <table border="1">
    *   <tr>
    *       <td><em>initSize</em></td>
    *       <td>Use the internal Xinha.initSize() method to calculate the editor's 
    *       dimensions. This is suitable for most usecases.</td>
    *   </tr>
    *   <tr>
    *       <td><em>restore</em></td>
    *       <td>The editor's dimensions will be stored before going into fullscreen
    *       mode and restored when we return to normal mode, taking a possible
    *       window resize during fullscreen in account.</td>
    *     </tr>
    * </table>
    *
    * Default: <code>"initSize"</code>
    * @type String
    */
  this.fullScreenSizeDownMethod = 'initSize';
  
  /** This array orders all buttons except plugin buttons in the toolbar. Plugin buttons typically look for one 
   *  a certain button in the toolbar and place themselves next to it.
   * Default value:
   *<pre>
   *xinha_config.toolbar =
   * [
   *   ["popupeditor"],
   *   ["separator","formatblock","fontname","fontsize","bold","italic","underline","strikethrough"],
   *   ["separator","forecolor","hilitecolor","textindicator"],
   *   ["separator","subscript","superscript"],
   *   ["linebreak","separator","justifyleft","justifycenter","justifyright","justifyfull"],
   *   ["separator","insertorderedlist","insertunorderedlist","outdent","indent"],
   *   ["separator","inserthorizontalrule","createlink","insertimage","inserttable"],
   *   ["linebreak","separator","undo","redo","selectall","print"], (Xinha.is_gecko ? [] : ["cut","copy","paste","overwrite","saveas"]),
   *   ["separator","killword","clearfonts","removeformat","toggleborders","splitblock","lefttoright", "righttoleft"],
   *   ["separator","htmlmode","showhelp","about"]
   * ];
   *</pre>
   * @type Array
   */  
  this.toolbar =
  [
    ["popupeditor"],
    ["separator","formatblock","fontname","fontsize","bold","italic","underline","strikethrough"],
    ["separator","forecolor","hilitecolor","textindicator"],
    ["separator","subscript","superscript"],
    ["linebreak","separator","justifyleft","justifycenter","justifyright","justifyfull"],
    ["separator","insertorderedlist","insertunorderedlist","outdent","indent"],
    ["separator","inserthorizontalrule","createlink","insertimage","inserttable"],
    ["linebreak","separator","undo","redo","selectall","print"], (Xinha.is_gecko ? [] : ["cut","copy","paste","overwrite","saveas"]),
    ["separator","killword","clearfonts","removeformat","toggleborders","splitblock","lefttoright", "righttoleft"],
    ["separator","htmlmode","showhelp","about"]
  ];

  /** The fontnames listed in the fontname dropdown
   * Default value:
   *<pre>
   *xinha_config.fontname =
   *{
   *  "&#8212; font &#8212;" : '',
   *  "Arial"                : 'arial,helvetica,sans-serif',
   *  "Courier New"          : 'courier new,courier,monospace',
   *  "Georgia"              : 'georgia,times new roman,times,serif',
   *  "Tahoma"               : 'tahoma,arial,helvetica,sans-serif',
   *  "Times New Roman"      : 'times new roman,times,serif',
   *  "Verdana"              : 'verdana,arial,helvetica,sans-serif',
   *  "impact"               : 'impact',
   *  "WingDings"            : 'wingdings'
   *};
   *</pre>
   * @type Object
   */
  this.fontname =
  {
    "&#8212; font &#8212;": "", // &#8212; is mdash
    "Arial"           :	'arial,helvetica,sans-serif',
    "Courier New"     :	'courier new,courier,monospace',
    "Georgia"         :	'georgia,times new roman,times,serif',
    "Tahoma"          :	'tahoma,arial,helvetica,sans-serif',
    "Times New Roman" : 'times new roman,times,serif',
    "Verdana"         :	'verdana,arial,helvetica,sans-serif',
    "impact"          :	'impact',
    "WingDings"       : 'wingdings' 
  };

  /** The fontsizes listed in the fontsize dropdown
   * Default value:
   *<pre>
   *xinha_config.fontsize =
   *{
   *  "&#8212; size &#8212;": "",
   *  "1 (8 pt)" : "1",
   *  "2 (10 pt)": "2",
   *  "3 (12 pt)": "3",
   *  "4 (14 pt)": "4",
   *  "5 (18 pt)": "5",
   *  "6 (24 pt)": "6",
   *  "7 (36 pt)": "7"
   *};
   *</pre>
   * @type Object
   */
  this.fontsize =
  {
    "&#8212; size &#8212;": "", // &#8212; is mdash
    "1 (8 pt)" : "1",
    "2 (10 pt)": "2",
    "3 (12 pt)": "3",
    "4 (14 pt)": "4",
    "5 (18 pt)": "5",
    "6 (24 pt)": "6",
    "7 (36 pt)": "7"
  };
  /** The tags listed in the formatblock dropdown
   * Default value:
   *<pre>
   *xinha_config.formatblock =
   *{
   *  "&#8212; format &#8212;": "", // &#8212; is mdash
   *  "Heading 1": "h1",
   *  "Heading 2": "h2",
   *  "Heading 3": "h3",
   *  "Heading 4": "h4",
   *  "Heading 5": "h5",
   *  "Heading 6": "h6",
   *  "Normal"   : "p",
   *  "Address"  : "address",
   *  "Formatted": "pre"
   *}
   *</pre>
   * @type Object
   */
  this.formatblock =
  {
    "&#8212; format &#8212;": "", // &#8212; is mdash
    "Heading 1": "h1",
    "Heading 2": "h2",
    "Heading 3": "h3",
    "Heading 4": "h4",
    "Heading 5": "h5",
    "Heading 6": "h6",
    "Normal"   : "p",
    "Address"  : "address",
    "Formatted": "pre"
  };

  /** You can provide custom functions that will be used to determine which of the
   * "formatblock" options is currently active and selected in the dropdown.
   *
   * Example:
   * <pre>
   * xinha_config.formatblockDetector['h5'] = function(xinha, currentElement)
   * {
   *   if (my_special_matching_logic(currentElement)) {
   *     return true;
   *   } else {
   *     return false;
   *   }
   * };
   * </pre>
   *
   * You probably don't want to mess with this, unless you are adding new, custom
   * "formatblock" options which don't correspond to real HTML tags.  If you want
   * to do that, you can use this configuration option to tell xinha how to detect
   * when it is within your custom context.
   *
   * For more, see: http://www.coactivate.org/projects/xinha/custom-formatblock-options
   */
  this.formatblockDetector = {};

  this.dialogOptions =
  {
    'centered' : true, //true: dialog is shown in the center the screen, false dialog is shown near the clicked toolbar button
    'greyout':true, //true: when showing modal dialogs, the page behind the dialoge is greyed-out
    'closeOnEscape':true
  };
  /** You can add functions to this object to be executed on specific events
   * Example:
   * <pre>
   * xinha_config.Events.onKeyPress = function (event)
   * {
   *    //do something 
   *    return false;
   * }
   * </pre>
   * Note that <em>this</em> inside the function refers to the respective Xinha object
   * The possible function names are documented at <a href="http://trac.xinha.org/wiki/Documentation/EventHooks">http://trac.xinha.org/wiki/Documentation/EventHooks</a>
   */
  this.Events = {};
  
  /** ??
   * Default: <code>{}</code>
   * @type Object
   */
  this.customSelects = {};

  /** Switches on some debugging (only in execCommand() as far as I see at the moment)<br />
   *
   * Default: <code>false</code>
   * @type Boolean
   */
  this.debug = false;

  this.URIs =
  {
   "blank": _editor_url + "popups/blank.html",
   "link":  _editor_url + "modules/CreateLink/link.html",
   "insert_image": _editor_url + "modules/InsertImage/insert_image.html",
   "insert_table":  _editor_url + "modules/InsertTable/insert_table.html",
   "select_color": _editor_url + "popups/select_color.html",
   "help": _editor_url + "popups/editor_help.html"
  };

   /** The button list conains the definitions of the toolbar button. Normally, there's nothing to change here :) 
   * <div style="white-space:pre">ADDING CUSTOM BUTTONS: please read below!
   * format of the btnList elements is "ID: [ ToolTip, Icon, Enabled in text mode?, ACTION ]"
   *    - ID: unique ID for the button.  If the button calls document.execCommand
   *	    it's wise to give it the same name as the called command.
   *    - ACTION: function that gets called when the button is clicked.
   *              it has the following prototype:
   *                 function(editor, buttonName)
   *              - editor is the Xinha object that triggered the call
   *              - buttonName is the ID of the clicked button
   *              These 2 parameters makes it possible for you to use the same
   *              handler for more Xinha objects or for more different buttons.
   *    - ToolTip: tooltip, will be translated below
   *    - Icon: path to an icon image file for the button
   *            OR; you can use an 18x18 block of a larger image by supllying an array
   *            that has three elemtents, the first is the larger image, the second is the column
   *            the third is the row.  The ros and columns numbering starts at 0 but there is
   *            a header row and header column which have numbering to make life easier.
   *            See images/buttons_main.gif to see how it's done.
   *    - Enabled in text mode: if false the button gets disabled for text-only mode; otherwise enabled all the time.</div>
   * @type Object
   */
  this.btnList =
  {
    bold: [ "Bold", Xinha._lc({key: 'button_bold', string: ["ed_buttons_main.png",3,2]}, 'Xinha'), false, function(e) { e.execCommand("bold"); } ],
    italic: [ "Italic", Xinha._lc({key: 'button_italic', string: ["ed_buttons_main.png",2,2]}, 'Xinha'), false, function(e) { e.execCommand("italic"); } ],
    underline: [ "Underline", Xinha._lc({key: 'button_underline', string: ["ed_buttons_main.png",2,0]}, 'Xinha'), false, function(e) { e.execCommand("underline"); } ],
    strikethrough: [ "Strikethrough", Xinha._lc({key: 'button_strikethrough', string: ["ed_buttons_main.png",3,0]}, 'Xinha'), false, function(e) { e.execCommand("strikethrough"); } ],
    subscript: [ "Subscript", Xinha._lc({key: 'button_subscript', string: ["ed_buttons_main.png",3,1]}, 'Xinha'), false, function(e) { e.execCommand("subscript"); } ],
    superscript: [ "Superscript", Xinha._lc({key: 'button_superscript', string: ["ed_buttons_main.png",2,1]}, 'Xinha'), false, function(e) { e.execCommand("superscript"); } ],

    justifyleft: [ "Justify Left", ["ed_buttons_main.png",0,0], false, function(e) { e.execCommand("justifyleft"); } ],
    justifycenter: [ "Justify Center", ["ed_buttons_main.png",1,1], false, function(e){ e.execCommand("justifycenter"); } ],
    justifyright: [ "Justify Right", ["ed_buttons_main.png",1,0], false, function(e) { e.execCommand("justifyright"); } ],
    justifyfull: [ "Justify Full", ["ed_buttons_main.png",0,1], false, function(e) { e.execCommand("justifyfull"); } ],

    orderedlist: [ "Ordered List", ["ed_buttons_main.png",0,3], false, function(e) { e.execCommand("insertorderedlist"); } ],
    unorderedlist: [ "Bulleted List", ["ed_buttons_main.png",1,3], false, function(e) { e.execCommand("insertunorderedlist"); } ],
    insertorderedlist: [ "Ordered List", ["ed_buttons_main.png",0,3], false, function(e) { e.execCommand("insertorderedlist"); } ],
    insertunorderedlist: [ "Bulleted List", ["ed_buttons_main.png",1,3], false, function(e) { e.execCommand("insertunorderedlist"); } ],

    outdent: [ "Decrease Indent", ["ed_buttons_main.png",1,2], false, function(e) { e.execCommand("outdent"); } ],
    indent: [ "Increase Indent",["ed_buttons_main.png",0,2], false, function(e) { e.execCommand("indent"); } ],
    forecolor: [ "Font Color", ["ed_buttons_main.png",3,3], false, function(e) { e.execCommand("forecolor"); } ],
    hilitecolor: [ "Background Color", ["ed_buttons_main.png",2,3], false, function(e) { e.execCommand("hilitecolor"); } ],

    undo: [ "Undoes your last action", ["ed_buttons_main.png",4,2], false, function(e) { e.execCommand("undo"); } ],
    redo: [ "Redoes your last action", ["ed_buttons_main.png",5,2], false, function(e) { e.execCommand("redo"); } ],
    cut: [ "Cut selection", ["ed_buttons_main.png",5,0], false,  function (e, cmd) { e.execCommand(cmd); } ],
    copy: [ "Copy selection", ["ed_buttons_main.png",4,0], false,  function (e, cmd) { e.execCommand(cmd); } ],
    paste: [ "Paste from clipboard", ["ed_buttons_main.png",4,1], false,  function (e, cmd) { e.execCommand(cmd); } ],
    selectall: [ "Select all", ["ed_buttons_main.png",3,5], false, function(e) {e.execCommand("selectall");} ],

    inserthorizontalrule: [ "Horizontal Rule", ["ed_buttons_main.png",6,0], false, function(e) { e.execCommand("inserthorizontalrule"); } ],
    createlink: [ "Insert Web Link", ["ed_buttons_main.png",6,1], false, function(e) { e.execCommand("createlink"); } ],
    insertimage: [ "Insert/Modify Image", ["ed_buttons_main.png",6,3], false, function(e) { e.execCommand("insertimage"); } ],
    inserttable: [ "Insert Table", ["ed_buttons_main.png",6,2], false, function(e) { e.execCommand("inserttable"); } ],

    htmlmode: [ "Toggle HTML Source", ["ed_buttons_main.png",7,0], true, function(e) { e.execCommand("htmlmode"); } ],
    toggleborders: [ "Toggle Borders", ["ed_buttons_main.png",7,2], false, function(e) { e._toggleBorders(); } ],
    print: [ "Print document", ["ed_buttons_main.png",8,1], false, function(e) { if(Xinha.is_gecko) {e._iframe.contentWindow.print(); } else { e.focusEditor(); print(); } } ],
    saveas: [ "Save as", ["ed_buttons_main.png",9,1], false, function(e) { e.execCommand("saveas",false,"noname.htm"); } ],
    about: [ "About this editor", ["ed_buttons_main.png",8,2], true, function(e) { e.getPluginInstance("AboutBox").show(); } ],
    showhelp: [ "Help using editor", ["ed_buttons_main.png",9,2], true, function(e) { e.execCommand("showhelp"); } ],

    splitblock: [ "Split Block", "ed_splitblock.gif", false, function(e) { e._splitBlock(); } ],
    lefttoright: [ "Direction left to right", ["ed_buttons_main.png",0,2], false, function(e) { e.execCommand("lefttoright"); } ],
    righttoleft: [ "Direction right to left", ["ed_buttons_main.png",1,2], false, function(e) { e.execCommand("righttoleft"); } ],
    overwrite: [ "Insert/Overwrite", "ed_overwrite.gif", false, function(e) { e.execCommand("overwrite"); } ],

    wordclean: [ "MS Word Cleaner", ["ed_buttons_main.png",5,3], false, function(e) { e._wordClean(); } ],
    clearfonts: [ "Clear Inline Font Specifications", ["ed_buttons_main.png",5,4], true, function(e) { e._clearFonts(); } ],
    removeformat: [ "Remove formatting", ["ed_buttons_main.png",4,4], false, function(e) { e.execCommand("removeformat"); } ],
    killword: [ "Clear MSOffice tags", ["ed_buttons_main.png",4,3], false, function(e) { e.execCommand("killword"); } ]
  };
  
  /** A hash of double click handlers for the given elements, each element may have one or more double click handlers
   *  called in sequence.  The element may contain a class selector ( a.somethingSpecial )
   *  
   */
   
  this.dblclickList = 
  {
      "a": [function(e, target) {e.execCommand("createlink", false, target);}],
    "img": [function(e, target) {e._insertImage(target);}]
  };

 /**
  * HTML class attribute to apply to the <body> tag within the editor's iframe.
  * If it is not specified, no class will be set.
  * 
  *  Default: <code>null</code>
  */
  this.bodyClass = null;

 /**
  * HTML ID attribute to apply to the <body> tag within the editor's iframe.
  * If it is not specified, no ID will be set.
  * 
  *  Default: <code>null</code>
  */
  this.bodyID = null;
  
  /** A container for additional icons that may be swapped within one button (like fullscreen)
   * @private
   */
  this.iconList = 
  {
    dialogCaption : _editor_url + 'images/xinha-small-icon.gif',
    wysiwygmode : [_editor_url + 'images/ed_buttons_main.png',7,1]
  };
  // initialize tooltips from the I18N module and generate correct image path
  for ( var i in this.btnList )
  {
    var btn = this.btnList[i];
    // prevent iterating over wrong type
    if ( typeof btn != 'object' )
    {
      continue;
    } 
    if ( typeof btn[1] != 'string' )
    {
      btn[1][0] = _editor_url + this.imgURL + btn[1][0];
    }
    else
    {
      btn[1] = _editor_url + this.imgURL + btn[1];
    }
    btn[0] = Xinha._lc(btn[0]); //initialize tooltip
  }
};
/** A plugin may require more than one icon for one button, this has to be registered in order to work with the iconsets (see FullScreen)
 * 
 * @param {String} id
 * @param {String|Array} icon definition like in registerButton
 */
Xinha.Config.prototype.registerIcon = function (id, icon)
{
  this.iconList[id] = icon;
};
/** ADDING CUSTOM BUTTONS
*   ---------------------
*
*
* Example on how to add a custom button when you construct the Xinha:
*
*   var editor = new Xinha("your_text_area_id");
*   var cfg = editor.config; // this is the default configuration
*   cfg.btnList["my-hilite"] =
*	[ "Highlight selection", // tooltip
*	  "my_hilite.gif", // image
*	  false // disabled in text mode
*	  function(editor) { editor.surroundHTML('<span style="background:yellow">', '</span>'); }, // action
*	];
*   cfg.toolbar.push(["linebreak", "my-hilite"]); // add the new button to the toolbar
*
* An alternate (also more convenient and recommended) way to
* accomplish this is to use the registerButton function below.
*/
/** Helper function: register a new button with the configuration.  It can be
 * called with all 5 arguments, or with only one (first one).  When called with
 * only one argument it must be an object with the following properties: id,
 * tooltip, image, textMode, action.<br />  
 * 
 * Examples:<br />
 *<pre>
 * config.registerButton("my-hilite", "Hilite text", "my-hilite.gif", false, function(editor) {...});
 * config.registerButton({
 *      id       : "my-hilite",      // the ID of your button
 *      tooltip  : "Hilite text",    // the tooltip
 *      image    : "my-hilite.gif",  // image to be displayed in the toolbar
 *      textMode : false,            // disabled in text mode
 *      action   : function(editor) { // called when the button is clicked
 *                   editor.surroundHTML('<span class="hilite">', '</span>');
 *                 },
 *      context  : "p"               // will be disabled if outside a <p> element
 *    });</pre>
 */
Xinha.Config.prototype.registerButton = function(id, tooltip, image, textMode, action, context)
{
  if ( typeof id == "string" )
  {
    this.btnList[id] = [ tooltip, image, textMode, action, context ];
  }
  else if ( typeof id == "object" )
  {
    this.btnList[id.id] = [ id.tooltip, id.image, id.textMode, id.action, id.context ];
  }
  else
  {
    alert("ERROR [Xinha.Config::registerButton]:\ninvalid arguments");
    return false;
  }
};

Xinha.prototype.registerPanel = function(side, object)
{
  if ( !side )
  {
    side = 'right';
  }
  this.setLoadingMessage('Register ' + side + ' panel ');
  var panel = this.addPanel(side);
  if ( object )
  {
    object.drawPanelIn(panel);
  }
};

/** The following helper function registers a dropdown box with the editor
 * configuration.  You still have to add it to the toolbar, same as with the
 * buttons.  Call it like this:
 *
 * FIXME: add example
 */
Xinha.Config.prototype.registerDropdown = function(object)
{
  // check for existing id
//  if ( typeof this.customSelects[object.id] != "undefined" )
//  {
    // alert("WARNING [Xinha.Config::registerDropdown]:\nA dropdown with the same ID already exists.");
//  }
//  if ( typeof this.btnList[object.id] != "undefined" )
//  {
    // alert("WARNING [Xinha.Config::registerDropdown]:\nA button with the same ID already exists.");
//  }
  this.customSelects[object.id] = object;
};

/** Call this function to remove some buttons/drop-down boxes from the toolbar.
 * Pass as the only parameter a string containing button/drop-down names
 * delimited by spaces.  Note that the string should also begin with a space
 * and end with a space.  Example:
 *
 *   config.hideSomeButtons(" fontname fontsize textindicator ");
 *
 * It's useful because it's easier to remove stuff from the defaul toolbar than
 * create a brand new toolbar ;-)
 */
Xinha.Config.prototype.hideSomeButtons = function(remove)
{
  var toolbar = this.toolbar;
  for ( var i = toolbar.length; --i >= 0; )
  {
    var line = toolbar[i];
    for ( var j = line.length; --j >= 0; )
    {
      if ( remove.indexOf(" " + line[j] + " ") >= 0 )
      {
        var len = 1;
        if ( /separator|space/.test(line[j + 1]) )
        {
          len = 2;
        }
        line.splice(j, len);
      }
    }
  }
};

/** Helper Function: add buttons/drop-downs boxes with title or separator to the toolbar
 * if the buttons/drop-downs boxes doesn't allready exists.
 * id: button or selectbox (as array with separator or title)
 * where: button or selectbox (as array if the first is not found take the second and so on)
 * position:
 * -1 = insert button (id) one position before the button (where)
 * 0 = replace button (where) by button (id)
 * +1 = insert button (id) one position after button (where)
 *
 * cfg.addToolbarElement(["T[title]", "button_id", "separator"] , ["first_id","second_id"], -1);
*/

Xinha.Config.prototype.addToolbarElement = function(id, where, position)
{
  var toolbar = this.toolbar;
  var a, i, j, o, sid;
  var idIsArray = false;
  var whereIsArray = false;
  var whereLength = 0;
  var whereJ = 0;
  var whereI = 0;
  var exists = false;
  var found = false;
  // check if id and where are arrys
  if ( ( id && typeof id == "object" ) && ( id.constructor == Array ) )
  {
    idIsArray = true;
  }
  if ( ( where && typeof where == "object" ) && ( where.constructor == Array ) )
  {
    whereIsArray = true;
    whereLength = where.length;
	}

  if ( idIsArray ) //find the button/select box in input array
  {
    for ( i = 0; i < id.length; ++i )
    {
      if ( ( id[i] != "separator" ) && ( id[i].indexOf("T[") !== 0) )
      {
        sid = id[i];
      }
    }
  }
  else
  {
    sid = id;
  }
  
  for ( i = 0; i < toolbar.length; ++i ) {
    a = toolbar[i];
    for ( j = 0; j < a.length; ++j ) {
      // check if button/select box exists
      if ( a[j] == sid ) {
        return; // cancel to add elements if same button already exists
      }
    }
  }
  

  for ( i = 0; !found && i < toolbar.length; ++i )
  {
    a = toolbar[i];
    for ( j = 0; !found && j < a.length; ++j )
    {
      if ( whereIsArray )
      {
        for ( o = 0; o < whereLength; ++o )
        {
          if ( a[j] == where[o] )
          {
            if ( o === 0 )
            {
              found = true;
              j--;
              break;
            }
            else
            {
              whereI = i;
              whereJ = j;
              whereLength = o;
            }
          }
        }
      }
      else
      {
        // find the position to insert
        if ( a[j] == where )
        { 
          found = true;
          break;
        }
      }
    }
  }

  //if check found any other as the first button
  if ( !found && whereIsArray )
  { 
    if ( where.length != whereLength )
    {
      j = whereJ;
      a = toolbar[whereI];
      found = true;
    }
  }
  if ( found )
  {
    // replace the found button
    if ( position === 0 )
    {
      if ( idIsArray)
      {
        a[j] = id[id.length-1];
        for ( i = id.length-1; --i >= 0; )
        {
          a.splice(j, 0, id[i]);
        }
      }
      else
      {
        a[j] = id;
      }
    }
    else
    { 
      // insert before/after the found button
      if ( position < 0 )
      {
        j = j + position + 1; //correct position before
      }
      else if ( position > 0 )
      {
        j = j + position; //correct posion after
      }
      if ( idIsArray )
      {
        for ( i = id.length; --i >= 0; )
        {
          a.splice(j, 0, id[i]);
        }
      }
      else
      {
        a.splice(j, 0, id);
      }
    }
  }
  else
  {
    // no button found
    toolbar[0].splice(0, 0, "separator");
    if ( idIsArray)
    {
      for ( i = id.length; --i >= 0; )
      {
        toolbar[0].splice(0, 0, id[i]);
      }
    }
    else
    {
      toolbar[0].splice(0, 0, id);
    }
  }
};
/** Alias of Xinha.Config.prototype.hideSomeButtons()
* @type Function
*/
Xinha.Config.prototype.removeToolbarElement = Xinha.Config.prototype.hideSomeButtons;

/** Helper function: replace all TEXTAREA-s in the document with Xinha-s. 
* @param {Xinha.Config} optional config 
*/
Xinha.replaceAll = function(config)
{
  var tas = document.getElementsByTagName("textarea");
  // @todo: weird syntax, doesnt help to read the code, doesnt obfuscate it and doesnt make it quicker, better rewrite this part
  for ( var i = tas.length; i > 0; new Xinha(tas[--i], config).generate() )
  {
    // NOP
  }
};

/** Helper function: replaces the TEXTAREA with the given ID with Xinha. 
* @param {string} id id of the textarea to replace 
* @param {Xinha.Config} optional config 
*/
Xinha.replace = function(id, config)
{
  var ta = Xinha.getElementById("textarea", id);
  return ta ? new Xinha(ta, config).generate() : null;
};
 
/** Creates the toolbar and appends it to the _htmlarea
* @private
* @returns {DomNode} toolbar
*/
Xinha.prototype._createToolbar = function ()
{
  this.setLoadingMessage(Xinha._lc('Create Toolbar'));
  var editor = this;	// to access this in nested functions

  var toolbar = document.createElement("div");
  // ._toolbar is for legacy, ._toolBar is better thanks.
  this._toolBar = this._toolbar = toolbar;
  toolbar.className = "toolbar";  
  toolbar.align = this.config.toolbarAlign;
  
  Xinha.freeLater(this, '_toolBar');
  Xinha.freeLater(this, '_toolbar');
  
  var tb_row = null;
  var tb_objects = {};
  this._toolbarObjects = tb_objects;

	this._createToolbar1(editor, toolbar, tb_objects);
	
	// IE8 is totally retarded, if you click on a toolbar element (eg button)
	// and it doesn't have unselectable="on", then it defocuses the editor losing the selection
	// so nothing works.  Particularly prevalent with TableOperations
	function noselect(e)
	{
    if(e.tagName) e.unselectable = "on";        
    if(e.childNodes)
    {
      for(var i = 0; i < e.childNodes.length; i++) if(e.tagName) noselect(e.childNodes[i]);
    }
	}
	if(Xinha.is_ie) noselect(toolbar);
	
	
	this._htmlArea.appendChild(toolbar);      
  
  return toolbar;
};

/** FIXME : function never used, can probably be removed from source
* @private
* @deprecated
*/
Xinha.prototype._setConfig = function(config)
{
	this.config = config;
};
/** FIXME: How can this be used??
* @private
*/
Xinha.prototype._rebuildToolbar = function()
{
	this._createToolbar1(this, this._toolbar, this._toolbarObjects);

  // We only want ONE editor at a time to be active
  if ( Xinha._currentlyActiveEditor )
  {
    if ( Xinha._currentlyActiveEditor == this )
    {
      this.activateEditor();
    }
  }
  else
  {
    this.disableToolbar();
  }
};

/**
 * Create a break element to add in the toolbar
 *
 * @return {DomNode} HTML element to add
 * @private
 */
Xinha._createToolbarBreakingElement = function()
{
  var brk = document.createElement('div');
  brk.style.height = '1px';
  brk.style.width = '1px';
  brk.style.lineHeight = '1px';
  brk.style.fontSize = '1px';
  brk.style.clear = 'both';
  return brk;
};


/** separate from previous createToolBar to allow dynamic change of toolbar
 * @private
 * @return {DomNode} toolbar
 */
Xinha.prototype._createToolbar1 = function (editor, toolbar, tb_objects)
{
  // We will clean out any existing toolbar elements.
  while (toolbar.lastChild)
  {
    toolbar.removeChild(toolbar.lastChild);
  }

  var tb_row;
  // This shouldn't be necessary, but IE seems to float outside of the container
  // when we float toolbar sections, so we have to clear:both here as well
  // as at the end (which we do have to do).
  if ( editor.config.flowToolbars )
  {
    toolbar.appendChild(Xinha._createToolbarBreakingElement());
  }

  // creates a new line in the toolbar
  function newLine()
  {
    if ( typeof tb_row != 'undefined' && tb_row.childNodes.length === 0)
    {
      return;
    }

    var table = document.createElement("table");
    table.border = "0px";
    table.cellSpacing = "0px";
    table.cellPadding = "0px";
    if ( editor.config.flowToolbars )
    {
      if ( Xinha.is_ie )
      {
        table.style.styleFloat = "left";
      }
      else
      {
        table.style.cssFloat = "left";
      }
    }

    toolbar.appendChild(table);
    // TBODY is required for IE, otherwise you don't see anything
    // in the TABLE.
    var tb_body = document.createElement("tbody");
    table.appendChild(tb_body);
    tb_row = document.createElement("tr");
    tb_body.appendChild(tb_row);

    table.className = 'toolbarRow'; // meh, kinda.
  } // END of function: newLine

  // init first line
  newLine();

  // updates the state of a toolbar element.  This function is member of
  // a toolbar element object (unnamed objects created by createButton or
  // createSelect functions below).
  function setButtonStatus(id, newval)
  {
    var oldval = this[id];
    var el = this.element;
    if ( oldval != newval )
    {
      switch (id)
      {
        case "enabled":
          if ( newval )
          {
            Xinha._removeClass(el, "buttonDisabled");
            el.disabled = false;
          }
          else
          {
            Xinha._addClass(el, "buttonDisabled");
            el.disabled = true;
          }
        break;
        case "active":
          if ( newval )
          {
            Xinha._addClass(el, "buttonPressed");
          }
          else
          {
            Xinha._removeClass(el, "buttonPressed");
          }
        break;
      }
      this[id] = newval;
    }
  } // END of function: setButtonStatus

  // this function will handle creation of combo boxes.  Receives as
  // parameter the name of a button as defined in the toolBar config.
  // This function is called from createButton, above, if the given "txt"
  // doesn't match a button.
  function createSelect(txt)
  {
    var options = null;
    var el = null;
    var cmd = null;
    var customSelects = editor.config.customSelects;
    var context = null;
    var tooltip = "";
    switch (txt)
    {
      case "fontsize":
      case "fontname":
      case "formatblock":
        // the following line retrieves the correct
        // configuration option because the variable name
        // inside the Config object is named the same as the
        // button/select in the toolbar.  For instance, if txt
        // == "formatblock" we retrieve config.formatblock (or
        // a different way to write it in JS is
        // config["formatblock"].
        options = editor.config[txt];
        cmd = txt;
      break;
      default:
        // try to fetch it from the list of registered selects
        cmd = txt;
        var dropdown = customSelects[cmd];
        if ( typeof dropdown != "undefined" )
        {
          options = dropdown.options;
          context = dropdown.context;
          if ( typeof dropdown.tooltip != "undefined" )
          {
            tooltip = dropdown.tooltip;
          }
        }
        else
        {
          alert("ERROR [createSelect]:\nCan't find the requested dropdown definition");
        }
      break;
    }
    if ( options )
    {
      el = document.createElement("select");
      el.title = tooltip;
      el.style.width = 'auto';
      el.name = txt;
      var obj =
      {
        name	: txt, // field name
        element : el,	// the UI element (SELECT)
        enabled : true, // is it enabled?
        text	: false, // enabled in text mode?
        cmd	: cmd, // command ID
        state	: setButtonStatus, // for changing state
        context : context
      };
      
      Xinha.freeLater(obj);
      
      tb_objects[txt] = obj;
      
      for ( var i in options )
      {
        // prevent iterating over wrong type
        if ( typeof options[i] != 'string' )
        {
          continue;
        }
        var op = document.createElement("option");
        op.innerHTML = Xinha._lc(i);
        op.value = options[i];
        if (txt =='fontname' && editor.config.showFontStylesInToolbar)
        {
          op.style.fontFamily = options[i];
        }
        el.appendChild(op);
      }
      Xinha._addEvent(el, "change", function () { editor._comboSelected(el, txt); } );
    }
    return el;
  } // END of function: createSelect

  // appends a new button to toolbar
  function createButton(txt)
  {
    // the element that will be created
    var el, btn, obj = null;
    switch (txt)
    {
      case "separator":
        if ( editor.config.flowToolbars )
        {
          newLine();
        }
        el = document.createElement("div");
        el.className = "separator";
      break;
      case "space":
        el = document.createElement("div");
        el.className = "space";
      break;
      case "linebreak":
        newLine();
        return false;
      case "textindicator":
        el = document.createElement("div");
        el.appendChild(document.createTextNode("A"));
        el.className = "indicator";
        el.title = Xinha._lc("Current style");
        obj =
        {
          name	: txt, // the button name (i.e. 'bold')
          element : el, // the UI element (DIV)
          enabled : true, // is it enabled?
          active	: false, // is it pressed?
          text	: false, // enabled in text mode?
          cmd	: "textindicator", // the command ID
          state	: setButtonStatus // for changing state
        };
      
        Xinha.freeLater(obj);
      
        tb_objects[txt] = obj;
      break;
      default:
        btn = editor.config.btnList[txt];
    }
    if ( !el && btn )
    {
      el = document.createElement("a");
      el.style.display = 'block';
      el.href = 'javascript:void(0)';
      el.style.textDecoration = 'none';
      el.title = btn[0];
      el.className = "button";
      el.style.direction = "ltr";
      // let's just pretend we have a button object, and
      // assign all the needed information to it.
      obj =
      {
        name : txt, // the button name (i.e. 'bold')
        element : el, // the UI element (DIV)
        enabled : true, // is it enabled?
        active : false, // is it pressed?
        text : btn[2], // enabled in text mode?
        cmd	: btn[3], // the command ID
        state	: setButtonStatus, // for changing state
        context : btn[4] || null // enabled in a certain context?
      };
      Xinha.freeLater(el);
      Xinha.freeLater(obj);

      tb_objects[txt] = obj;

      // prevent drag&drop of the icon to content area
      el.ondrag = function() { return false; };

      // handlers to emulate nice flat toolbar buttons
      Xinha._addEvent(
        el,
        "mouseout",
        function(ev)
        {
          if ( obj.enabled )
          {
            //Xinha._removeClass(el, "buttonHover");
            Xinha._removeClass(el, "buttonActive");
            if ( obj.active )
            {
              Xinha._addClass(el, "buttonPressed");
            }
          }
        }
      );

      Xinha._addEvent(
        el,
        "mousedown",
        function(ev)
        {
          if ( obj.enabled )
          {
            Xinha._addClass(el, "buttonActive");
            Xinha._removeClass(el, "buttonPressed");
            Xinha._stopEvent(Xinha.is_ie ? window.event : ev);
          }
        }
      );

      // when clicked, do the following:
      Xinha._addEvent(
        el,
        "click",
        function(ev)
        {
          ev = ev || window.event;
          editor.btnClickEvent = {clientX : ev.clientX, clientY : ev.clientY};
          if ( obj.enabled )
          {
            Xinha._removeClass(el, "buttonActive");
            //Xinha._removeClass(el, "buttonHover");
            if ( Xinha.is_gecko )
            {
              editor.activateEditor();
            }
            // We pass the event to the action so they can can use it to
            // enhance the UI (e.g. respond to shift or ctrl-click)
            obj.cmd(editor, obj.name, obj, ev);
            Xinha._stopEvent(ev);
          }
        }
      );

      var i_contain = Xinha.makeBtnImg(btn[1]);
      var img = i_contain.firstChild;
      Xinha.freeLater(i_contain);
      Xinha.freeLater(img);
      
      el.appendChild(i_contain);

      obj.imgel = img;      
      obj.swapImage = function(newimg)
      {
        if ( typeof newimg != 'string' )
        {
          img.src = newimg[0];
          img.style.position = 'relative';
          img.style.top  = newimg[2] ? ('-' + (18 * (newimg[2] + 1)) + 'px') : '-18px';
          img.style.left = newimg[1] ? ('-' + (18 * (newimg[1] + 1)) + 'px') : '-18px';
        }
        else
        {
          obj.imgel.src = newimg;
          img.style.top = '0px';
          img.style.left = '0px';
        }
      };
      
    }
    else if( !el )
    {
      el = createSelect(txt);
    }

    return el;
  }

  var first = true;
  for ( var i = 0; i < this.config.toolbar.length; ++i )
  {
    if ( !first )
    {
      // createButton("linebreak");
    }
    else
    {
      first = false;
    }
    if ( this.config.toolbar[i] === null )
    {
      this.config.toolbar[i] = ['separator'];
    }
    var group = this.config.toolbar[i];

    for ( var j = 0; j < group.length; ++j )
    {
      var code = group[j];
      var tb_cell;
      if ( /^([IT])\[(.*?)\]/.test(code) )
      {
        // special case, create text label
        var l7ed = RegExp.$1 == "I"; // localized?
        var label = RegExp.$2;
        if ( l7ed )
        {
          label = Xinha._lc(label);
        }
        tb_cell = document.createElement("td");
        tb_row.appendChild(tb_cell);
        tb_cell.className = "label";
        tb_cell.innerHTML = label;
      }
      else if ( typeof code != 'function' )
      {
        var tb_element = createButton(code);
        if ( tb_element )
        {
          tb_cell = document.createElement("td");
          tb_cell.className = 'toolbarElement';
          tb_row.appendChild(tb_cell);
          tb_cell.appendChild(tb_element);
        }
        else if ( tb_element === null )
        {
          alert("FIXME: Unknown toolbar item: " + code);
        }
      }
    }
  }

  if ( editor.config.flowToolbars )
  {
    toolbar.appendChild(Xinha._createToolbarBreakingElement());
  }

  return toolbar;
};

/** creates a button (i.e. container element + image)
 * @private
 * @return {DomNode} conteainer element
 */
Xinha.makeBtnImg = function(imgDef, doc)
{
  if ( !doc )
  {
    doc = document;
  }

  if ( !doc._xinhaImgCache )
  {
    doc._xinhaImgCache = {};
    Xinha.freeLater(doc._xinhaImgCache);
  }

  var i_contain = null;
  if ( Xinha.is_ie && ( ( !doc.compatMode ) || ( doc.compatMode && doc.compatMode == "BackCompat" ) ) )
  {
    i_contain = doc.createElement('span');
  }
  else
  {
    i_contain = doc.createElement('div');
    i_contain.style.position = 'relative';
  }

  i_contain.style.overflow = 'hidden';
  i_contain.style.width = "18px";
  i_contain.style.height = "18px";
  i_contain.className = 'buttonImageContainer';

  var img = null;
  if ( typeof imgDef == 'string' )
  {
    if ( doc._xinhaImgCache[imgDef] )
    {
      img = doc._xinhaImgCache[imgDef].cloneNode();
    }
    else
    {
      if (Xinha.ie_version < 7 && /\.png$/.test(imgDef[0]))
      {
        img = doc.createElement("span");
      
        img.style.display = 'block';
        img.style.width = '18px';
        img.style.height = '18px';
        img.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+imgDef+'")';
		img.unselectable = 'on';
      }
      else
      {
        img = doc.createElement("img");
        img.src = imgDef;
      }
    }
  }
  else
  {
    if ( doc._xinhaImgCache[imgDef[0]] )
    {
      img = doc._xinhaImgCache[imgDef[0]].cloneNode();
    }
    else
    {
      if (Xinha.ie_version < 7 && /\.png$/.test(imgDef[0]))
      {
        img = doc.createElement("span");
        img.style.display = 'block';
        img.style.width = '18px';
        img.style.height = '18px';
        img.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+imgDef[0]+'")';
		img.unselectable = 'on';
      }
      else
      {
        img = doc.createElement("img");
        img.src = imgDef[0];
      }
      img.style.position = 'relative';
    }
    // @todo: Using 18 dont let us use a theme with its own icon toolbar height
    //        and width. Probably better to calculate this value 18
    //        var sizeIcon = img.width / nb_elements_per_image;
    img.style.top  = imgDef[2] ? ('-' + (18 * (imgDef[2] + 1)) + 'px') : '-18px';
    img.style.left = imgDef[1] ? ('-' + (18 * (imgDef[1] + 1)) + 'px') : '-18px';
  }
  i_contain.appendChild(img);
  return i_contain;
};
/** creates the status bar 
 * @private
 * @return {DomNode} status bar
 */
Xinha.prototype._createStatusBar = function()
{
  // TODO: Move styling into separate stylesheet
  this.setLoadingMessage(Xinha._lc('Create Statusbar'));
  var statusBar = document.createElement("div");
  statusBar.style.position = "relative";
  statusBar.className = "statusBar";
  statusBar.style.width = "100%";
  Xinha.freeLater(this, '_statusBar');

  var widgetContainer = document.createElement("div");
  widgetContainer.className = "statusBarWidgetContainer";
  widgetContainer.style.position = "absolute";
  widgetContainer.style.right = "0";
  widgetContainer.style.top = "0";
  widgetContainer.style.padding = "3px 3px 3px 10px";
  statusBar.appendChild(widgetContainer);

  // statusbar.appendChild(document.createTextNode(Xinha._lc("Path") + ": "));
  // creates a holder for the path view
  var statusBarTree = document.createElement("span");
  statusBarTree.className = "statusBarTree";
  statusBarTree.innerHTML = Xinha._lc("Path") + ": ";

  this._statusBarTree = statusBarTree;
  Xinha.freeLater(this, '_statusBarTree');
  statusBar.appendChild(statusBarTree);
  var statusBarTextMode = document.createElement("span");
  statusBarTextMode.innerHTML = Xinha.htmlEncode(Xinha._lc("You are in TEXT MODE.  Use the [<>] button to switch back to WYSIWYG."));
  statusBarTextMode.style.display = "none";

  this._statusBarTextMode = statusBarTextMode;
  Xinha.freeLater(this, '_statusBarTextMode');
  statusBar.appendChild(statusBarTextMode);

  statusBar.style.whiteSpace = "nowrap";

  var self = this;
  this.notifyOn("before_resize", function(evt, size) {
    self._statusBar.style.width = null;
  });
  this.notifyOn("resize", function(evt, size) {
    // HACK! IE6 doesn't update the width properly when resizing if it's 
    // given in pixels, but does hide the overflow content correctly when 
    // using 100% as the width. (FF, Safari and IE7 all require fixed
    // pixel widths to do the overflow hiding correctly.)
    if (Xinha.is_ie && Xinha.ie_version == 6)
    {
      self._statusBar.style.width = "100%";
    } 
    else
    {
      var width = size['width'];
      self._statusBar.style.width = width + "px";
    }
  });

  this.notifyOn("modechange", function(evt, mode) {
    // Loop through all registered status bar items
    // and show them only if they're turned on for
    // the new mode.
    for (var i in self._statusWidgets)
    {
      var widget = self._statusWidgets[i];
      for (var index=0; index<widget.modes.length; index++)
      {
        if (widget.modes[index] == mode.mode)
        {
          var found = true;
        }
      }
      if (typeof found == 'undefined')
      {
        widget.block.style.display = "none";  
      }
      else
      {
        widget.block.style.display = "";
      }
    }
  });

  if ( !this.config.statusBar )
  {
    // disable it...
    statusBar.style.display = "none";
  }
  return statusBar;
};

/** Registers and inserts a new block for a widget in the status bar
 @param id unique string identifer for this block
 @param modes list of modes this block should be shown in

 @returns reference to HTML element inserted into the status bar
 */
Xinha.prototype.registerStatusWidget = function(id, modes)
{
  modes = modes || ['wysiwyg'];
  if (!this._statusWidgets)
  {
    this._statusWidgets = {};
  }

  var block = document.createElement("div");
  block.className = "statusBarWidget";
  block = this._statusBar.firstChild.appendChild(block);

  var showWidget = false;
  for (var i=0; i<modes.length; i++)
  {
    if (modes[i] == this._editMode)
    {
      showWidget = true;
    }
  }
  block.style.display = showWidget == true ? "" : "none";

  this._statusWidgets[id] = {block: block, modes: modes};
  return block;
};

/** Creates the Xinha object and replaces the textarea with it. Loads required files.
 *  @returns {Boolean}
 */
Xinha.prototype.generate = function ()
{
  if ( !Xinha.isSupportedBrowser )
  {
    return;
  }
  
  var i;
  var editor = this;  // we'll need "this" in some nested functions
  var url;
  var found = false;
  var links = document.getElementsByTagName("link");

  if (!document.getElementById("XinhaCoreDesign"))
  {
    _editor_css = (typeof _editor_css == "string") ? _editor_css : "Xinha.css";
    for(i = 0; i<links.length; i++)
    {
      if ( ( links[i].rel == "stylesheet" ) && ( links[i].href == _editor_url + _editor_css ) )
      {
        found = true;
      }
    }
    if ( !found )
    {
      Xinha.loadStyle(_editor_css,null,"XinhaCoreDesign",true);
    }
  }
  
  if ( _editor_skin !== "" && !document.getElementById("XinhaSkin"))
  {
    found = false;
    for(i = 0; i<links.length; i++)
    {
      if ( ( links[i].rel == "stylesheet" ) && ( links[i].href == _editor_url + 'skins/' + _editor_skin + '/skin.css' ) )
      {
        found = true;
      }
    }
    if ( !found )
    {
      Xinha.loadStyle('skins/' + _editor_skin + '/skin.css',null,"XinhaSkin");
    }
  }
  var callback = function() { editor.generate(); };
  // Now load a specific browser plugin which will implement the above for us.
  if (Xinha.is_ie)
  {
    url = _editor_url + 'modules/InternetExplorer/InternetExplorer.js';
    if ( !Xinha.loadPlugins([{plugin:"InternetExplorer",url:url}], callback ) )
    {            
      return false;
    }
    if (!this.plugins.InternetExplorer)
    {
      editor._browserSpecificPlugin = editor.registerPlugin('InternetExplorer');
    }
  }
  else if (Xinha.is_webkit)
  {
    url = _editor_url + 'modules/WebKit/WebKit.js';
    if ( !Xinha.loadPlugins([{plugin:"WebKit",url:url}], callback ) )
    {
      return false;
    }
    if (!this.plugins.Webkit)
    {
      editor._browserSpecificPlugin = editor.registerPlugin('WebKit');
    }
  }
  else if (Xinha.is_opera)
  {
    url = _editor_url + 'modules/Opera/Opera.js';
    if ( !Xinha.loadPlugins([{plugin:"Opera",url:url}], callback ) )
    {            
      return false;
    }
    if (!this.plugins.Opera)
    {
      editor._browserSpecificPlugin = editor.registerPlugin('Opera');
    }
  }
  else if (Xinha.is_gecko)
  {
    url = _editor_url + 'modules/Gecko/Gecko.js';
    if ( !Xinha.loadPlugins([{plugin:"Gecko",url:url}], callback ) )
    {            
      return false;
    }
    if (!this.plugins.Gecko) 
    {
      editor._browserSpecificPlugin = editor.registerPlugin('Gecko');
    }
  }

  if ( typeof Dialog == 'undefined' && !Xinha._loadback( _editor_url + 'modules/Dialogs/dialog.js', callback, this ) )
  {    
    return false;
  }

  if ( typeof Xinha.Dialog == 'undefined' &&  !Xinha._loadback( _editor_url + 'modules/Dialogs/XinhaDialog.js' , callback, this ) )
  {    
    return false;
  }
  
  url = _editor_url + 'modules/FullScreen/full-screen.js';
  if ( !Xinha.loadPlugins([{plugin:"FullScreen",url:url}], callback ))
  {
    return false;
  }
  
  url = _editor_url + 'modules/ColorPicker/ColorPicker.js';
  if ( !Xinha.loadPlugins([{plugin:"ColorPicker",url:url}], callback ) )
  {
    return false;
  }
  else if ( typeof Xinha.getPluginConstructor('ColorPicker') != 'undefined' && !this.plugins.colorPicker)
  {
    editor.registerPlugin('ColorPicker');
  }

  var toolbar = editor.config.toolbar;
  for ( i = toolbar.length; --i >= 0; )
  {
    for ( var j = toolbar[i].length; --j >= 0; )
    {
      switch (toolbar[i][j])
      {
        case "popupeditor":
        case "fullscreen":
          if (!this.plugins.FullScreen) 
          {
            editor.registerPlugin('FullScreen');
          }
        break;
        case "insertimage":
          url = _editor_url + 'modules/InsertImage/insert_image.js';
          if ( typeof Xinha.prototype._insertImage == 'undefined' && !Xinha.loadPlugins([{plugin:"InsertImage",url:url}], callback ) )
          {
            return false;
          }
          else if ( typeof Xinha.getPluginConstructor('InsertImage') != 'undefined' && !this.plugins.InsertImage)
          {
            editor.registerPlugin('InsertImage');
          }
        break;
        case "createlink":
          url = _editor_url + 'modules/CreateLink/link.js';
          if ( typeof Xinha.getPluginConstructor('Linker') == 'undefined' && !Xinha.loadPlugins([{plugin:"CreateLink",url:url}], callback ))
          {
            return false;
          }
          else if ( typeof Xinha.getPluginConstructor('CreateLink') != 'undefined' && !this.plugins.CreateLink) 
          {
            editor.registerPlugin('CreateLink');
          }
        break;
        case "inserttable":
          url = _editor_url + 'modules/InsertTable/insert_table.js';
          if ( !Xinha.loadPlugins([{plugin:"InsertTable",url:url}], callback ) )
          {
            return false;
          }
          else if ( typeof Xinha.getPluginConstructor('InsertTable') != 'undefined' && !this.plugins.InsertTable)
          {
            editor.registerPlugin('InsertTable');
          }
        break;
        case "about":
          url = _editor_url + 'modules/AboutBox/AboutBox.js';
          if ( !Xinha.loadPlugins([{plugin:"AboutBox",url:url}], callback ) )
          {
            return false;
          }
          else if ( typeof Xinha.getPluginConstructor('AboutBox') != 'undefined' && !this.plugins.AboutBox)
          {
            editor.registerPlugin('AboutBox');
          }
        break;
      }
    }
  }

  // If this is gecko, set up the paragraph handling now
  if ( Xinha.is_gecko &&  editor.config.mozParaHandler != 'built-in' )
  {
    if (  !Xinha.loadPlugins([{plugin:"EnterParagraphs",url: _editor_url + 'modules/Gecko/paraHandlerBest.js'}], callback ) )
    {
      return false;
    }
    if (!this.plugins.EnterParagraphs) 
    {
      editor.registerPlugin('EnterParagraphs');
    }
  }
  var getHtmlMethodPlugin = this.config.getHtmlMethod == 'TransformInnerHTML' ? _editor_url + 'modules/GetHtml/TransformInnerHTML.js' :  _editor_url + 'modules/GetHtml/DOMwalk.js';

  if ( !Xinha.loadPlugins([{plugin:"GetHtmlImplementation",url:getHtmlMethodPlugin}], callback))
  {
    return false;
  }
  else if (!this.plugins.GetHtmlImplementation)
  {
    editor.registerPlugin('GetHtmlImplementation');
  }
  function getTextContent(node)
  {
    return node.textContent || node.text;
  }
  if (_editor_skin)
  {
    this.skinInfo = {};
    var skinXML = Xinha._geturlcontent(_editor_url + 'skins/' + _editor_skin + '/skin.xml', true);
    if (skinXML)
    {
      var meta = skinXML.getElementsByTagName('meta');
      for (i=0;i<meta.length;i++)
      {
        this.skinInfo[meta[i].getAttribute('name')] = meta[i].getAttribute('value');
      }
      var recommendedIcons = skinXML.getElementsByTagName('recommendedIcons');
      if (!_editor_icons && recommendedIcons.length && getTextContent(recommendedIcons[0]))
      {
        _editor_icons = getTextContent(recommendedIcons[0]);
      }
    }
  }
  if (_editor_icons) 
  {
    var iconsXML = Xinha._geturlcontent(_editor_url + 'iconsets/' + _editor_icons + '/iconset.xml', true);

    if (iconsXML)
    {
      var icons = iconsXML.getElementsByTagName('icon');
      var icon, id, path, type, x, y;

      for (i=0;i<icons.length;i++)
      {
        icon = icons[i];
        id = icon.getAttribute('id');
        
        if (icon.getElementsByTagName(_editor_lang).length)
        {
          icon = icon.getElementsByTagName(_editor_lang)[0];
        }
        else
        {
          icon = icon.getElementsByTagName('default')[0];
        }
        path = getTextContent(icon.getElementsByTagName('path')[0]);
        path = (!/^\//.test(path) ? _editor_url : '') + path;
        type = icon.getAttribute('type');
        if (type == 'map')
        {
          x = parseInt(getTextContent(icon.getElementsByTagName('x')[0]), 10);
          y = parseInt(getTextContent(icon.getElementsByTagName('y')[0]), 10);
          if (this.config.btnList[id]) 
          {
            this.config.btnList[id][1] = [path, x, y];
          }
          if (this.config.iconList[id]) 
          {
            this.config.iconList[id] = [path, x, y];
          }
          
        }
        else
        {
          if (this.config.btnList[id]) 
          {
            this.config.btnList[id][1] = path;
          }
          if (this.config.iconList[id]) 
          {
            this.config.iconList[id] = path;
          }
        }
      }
    }
  }
  
  // create the editor framework, yah, table layout I know, but much easier
  // to get it working correctly this way, sorry about that, patches welcome.
  
  this.setLoadingMessage(Xinha._lc('Generate Xinha framework'));
  
  this._framework =
  {
    'table':   document.createElement('table'),
    'tbody':   document.createElement('tbody'), // IE will not show the table if it doesn't have a tbody!
    'tb_row':  document.createElement('tr'),
    'tb_cell': document.createElement('td'), // Toolbar

    'tp_row':  document.createElement('tr'),
    'tp_cell': this._panels.top.container,   // top panel

    'ler_row': document.createElement('tr'),
    'lp_cell': this._panels.left.container,  // left panel
    'ed_cell': document.createElement('td'), // editor
    'rp_cell': this._panels.right.container, // right panel

    'bp_row':  document.createElement('tr'),
    'bp_cell': this._panels.bottom.container,// bottom panel

    'sb_row':  document.createElement('tr'),
    'sb_cell': document.createElement('td')  // status bar

  };
  Xinha.freeLater(this._framework);
  
  var fw = this._framework;
  fw.table.border = "0";
  fw.table.cellPadding = "0";
  fw.table.cellSpacing = "0";

  fw.tb_row.style.verticalAlign = 'top';
  fw.tp_row.style.verticalAlign = 'top';
  fw.ler_row.style.verticalAlign= 'top';
  fw.bp_row.style.verticalAlign = 'top';
  fw.sb_row.style.verticalAlign = 'top';
  fw.ed_cell.style.position     = 'relative';

  // Put the cells in the rows        set col & rowspans
  // note that I've set all these so that all panels are showing
  // but they will be redone in sizeEditor() depending on which
  // panels are shown.  It's just here to clarify how the thing
  // is put togethor.
  fw.tb_row.appendChild(fw.tb_cell);
  fw.tb_cell.colSpan = 3;

  fw.tp_row.appendChild(fw.tp_cell);
  fw.tp_cell.colSpan = 3;

  fw.ler_row.appendChild(fw.lp_cell);
  fw.ler_row.appendChild(fw.ed_cell);
  fw.ler_row.appendChild(fw.rp_cell);

  fw.bp_row.appendChild(fw.bp_cell);
  fw.bp_cell.colSpan = 3;

  fw.sb_row.appendChild(fw.sb_cell);
  fw.sb_cell.colSpan = 3;

  // Put the rows in the table body
  fw.tbody.appendChild(fw.tb_row);  // Toolbar
  fw.tbody.appendChild(fw.tp_row); // Left, Top, Right panels
  fw.tbody.appendChild(fw.ler_row);  // Editor/Textarea
  fw.tbody.appendChild(fw.bp_row);  // Bottom panel
  fw.tbody.appendChild(fw.sb_row);  // Statusbar

  // and body in the table
  fw.table.appendChild(fw.tbody);

  var xinha = fw.table;
  this._htmlArea = xinha;
  Xinha.freeLater(this, '_htmlArea');
  xinha.className = "htmlarea";

    // create the toolbar and put in the area
  fw.tb_cell.appendChild( this._createToolbar() );

    // create the IFRAME & add to container
  var iframe = document.createElement("iframe");
  iframe.src = this.popupURL(editor.config.URIs.blank);
  iframe.id = "XinhaIFrame_" + this._textArea.id;
  fw.ed_cell.appendChild(iframe);
  this._iframe = iframe;
  this._iframe.className = 'xinha_iframe';
  Xinha.freeLater(this, '_iframe');
  
    // creates & appends the status bar
  var statusbar = this._createStatusBar();
  this._statusBar = fw.sb_cell.appendChild(statusbar);


  // insert Xinha before the textarea.
  var textarea = this._textArea;
  textarea.parentNode.insertBefore(xinha, textarea);
  textarea.className = 'xinha_textarea';

  // extract the textarea and insert it into the xinha framework
  Xinha.removeFromParent(textarea);
  fw.ed_cell.appendChild(textarea);

  // if another editor is activated while this one is in text mode, toolbar is disabled   
  Xinha.addDom0Event(
  this._textArea,
  'click',
  function()
  {
  	if ( Xinha._currentlyActiveEditor != this)
  	{
  	  editor.updateToolbar();
  	}
    return true;
  });
  
  // Set up event listeners for saving the iframe content to the textarea
  if ( textarea.form )
  {
    // onsubmit get the Xinha content and update original textarea.
    Xinha.prependDom0Event(
      this._textArea.form,
      'submit',
      function()
      {
        editor.firePluginEvent('onBeforeSubmit');
        editor._textArea.value = editor.outwardHtml(editor.getHTML());
        return true;
      }
    );

    var initialTAContent = textarea.value;

    // onreset revert the Xinha content to the textarea content
    Xinha.prependDom0Event(
      this._textArea.form,
      'reset',
      function()
      {
        editor.setHTML(editor.inwardHtml(initialTAContent));
        editor.updateToolbar();
        return true;
      }
    );

    // attach onsubmit handler to form.submit()
    // note: catch error in IE if any form element has id="submit"
    if ( !textarea.form.xinha_submit )
    {
      try 
      {
        textarea.form.xinha_submit = textarea.form.submit;
        textarea.form.submit = function() 
        {
          this.onsubmit();
          this.xinha_submit();
        };
      } catch(ex) {}
    }
  }

  // add a handler for the "back/forward" case -- on body.unload we save
  // the HTML content into the original textarea and restore it in its place.
  // apparently this does not work in IE?
  Xinha.prependDom0Event(
    window,
    'unload',
    function()
    {
      editor.firePluginEvent('onBeforeUnload');
      textarea.value = editor.outwardHtml(editor.getHTML());
      if (!Xinha.is_ie)
      {
        xinha.parentNode.replaceChild(textarea,xinha);
      }
      return true;
    }
  );

  // Hide textarea
  textarea.style.display = "none";

  // Initalize size
  editor.initSize();
  this.setLoadingMessage(Xinha._lc('Finishing'));
  // Add an event to initialize the iframe once loaded.
  editor._iframeLoadDone = false;
  if (Xinha.is_opera) 
  {
    editor.initIframe();
  }
  else 
  {
    Xinha._addEvent(
      this._iframe,
      'load',
      function(e)
      {
        if ( !editor._iframeLoadDone )
        {
          editor._iframeLoadDone = true;
          editor.initIframe();
        }
        return true;
      }
    );
  }
};

/**
 * Size the editor according to the INITIAL sizing information.
 * config.width
 *    The width may be set via three ways
 *    auto    = the width is inherited from the original textarea
 *    toolbar = the width is set to be the same size as the toolbar
 *    <set size> = the width is an explicit size (any CSS measurement, eg 100em should be fine)
 *
 * config.height
 *    auto    = the height is inherited from the original textarea
 *    <set size> = an explicit size measurement (again, CSS measurements)
 *
 * config.sizeIncludesBars
 *    true    = the tool & status bars will appear inside the width & height confines
 *    false   = the tool & status bars will appear outside the width & height confines
 *
 * @private
 */

Xinha.prototype.initSize = function()
{
  this.setLoadingMessage(Xinha._lc('Init editor size'));
  var editor = this;
  var width = null;
  var height = null;

  switch ( this.config.width )
  {
    case 'auto':
      width = this._initial_ta_size.w;
    break;

    case 'toolbar':
      width = this._toolBar.offsetWidth + 'px';
    break;

    default :
      // @todo: check if this is better :
      // width = (parseInt(this.config.width, 10) == this.config.width)? this.config.width + 'px' : this.config.width;
      width = /[^0-9]/.test(this.config.width) ? this.config.width : this.config.width + 'px';
    break;
  }
      // @todo: check if this is better :
      // height = (parseInt(this.config.height, 10) == this.config.height)? this.config.height + 'px' : this.config.height;
  height = this.config.height == 'auto' ? this._initial_ta_size.h : /[^0-9]/.test(this.config.height) ? this.config.height : this.config.height + 'px';
  
  this.sizeEditor(width, height, this.config.sizeIncludesBars, this.config.sizeIncludesPanels);

  // why can't we use the following line instead ?
//  this.notifyOn('panel_change',this.sizeEditor);
  this.notifyOn('panel_change',function() { editor.sizeEditor(); });
};

/**
 *  Size the editor to a specific size, or just refresh the size (when window resizes for example)
 *  @param {string} width optional width (CSS specification)
 *  @param {string} height optional height (CSS specification)
 *  @param {Boolean} includingBars optional to indicate if the size should include or exclude tool & status bars
 *  @param {Boolean} includingPanels optional to indicate if the size should include or exclude panels
 */
Xinha.prototype.sizeEditor = function(width, height, includingBars, includingPanels)
{
  if (this._risizing) 
  {
    return;
  }
  this._risizing = true;
  
  var framework = this._framework;
  
  this.notifyOf('before_resize', {width:width, height:height});
  this.firePluginEvent('onBeforeResize', width, height);
  // We need to set the iframe & textarea to 100% height so that the htmlarea
  // isn't "pushed out" when we get it's height, so we can change them later.
  this._iframe.style.height   = '100%';
  //here 100% can lead to an effect that the editor is considerably higher in text mode
  this._textArea.style.height = '1px';
  
  this._iframe.style.width    = '0px';
  this._textArea.style.width  = '0px';

  if ( includingBars !== null )
  {
    this._htmlArea.sizeIncludesToolbars = includingBars;
  }
  if ( includingPanels !== null )
  {
    this._htmlArea.sizeIncludesPanels = includingPanels;
  }

  if ( width )
  {
    this._htmlArea.style.width = width;
    if ( !this._htmlArea.sizeIncludesPanels )
    {
      // Need to add some for l & r panels
      var rPanel = this._panels.right;
      if ( rPanel.on && rPanel.panels.length && Xinha.hasDisplayedChildren(rPanel.div) )
      {
        this._htmlArea.style.width = (this._htmlArea.offsetWidth + parseInt(this.config.panel_dimensions.right, 10)) + 'px';
      }

      var lPanel = this._panels.left;
      if ( lPanel.on && lPanel.panels.length && Xinha.hasDisplayedChildren(lPanel.div) )
      {
        this._htmlArea.style.width = (this._htmlArea.offsetWidth + parseInt(this.config.panel_dimensions.left, 10)) + 'px';
      }
    }
  }

  if ( height )
  {
    this._htmlArea.style.height = height;
    if ( !this._htmlArea.sizeIncludesToolbars )
    {
      // Need to add some for toolbars
      this._htmlArea.style.height = (this._htmlArea.offsetHeight + this._toolbar.offsetHeight + this._statusBar.offsetHeight) + 'px';
    }

    if ( !this._htmlArea.sizeIncludesPanels )
    {
      // Need to add some for t & b panels
      var tPanel = this._panels.top;
      if ( tPanel.on && tPanel.panels.length && Xinha.hasDisplayedChildren(tPanel.div) )
      {
        this._htmlArea.style.height = (this._htmlArea.offsetHeight + parseInt(this.config.panel_dimensions.top, 10)) + 'px';
      }

      var bPanel = this._panels.bottom;
      if ( bPanel.on && bPanel.panels.length && Xinha.hasDisplayedChildren(bPanel.div) )
      {
        this._htmlArea.style.height = (this._htmlArea.offsetHeight + parseInt(this.config.panel_dimensions.bottom, 10)) + 'px';
      }
    }
  }

  // At this point we have this._htmlArea.style.width & this._htmlArea.style.height
  // which are the size for the OUTER editor area, including toolbars and panels
  // now we size the INNER area and position stuff in the right places.
  width  = this._htmlArea.offsetWidth;
  height = this._htmlArea.offsetHeight;

  // Set colspan for toolbar, and statusbar, rowspan for left & right panels, and insert panels to be displayed
  // into thier rows
  var panels = this._panels;
  var editor = this;
  var col_span = 1;

  function panel_is_alive(pan)
  {
    if ( panels[pan].on && panels[pan].panels.length && Xinha.hasDisplayedChildren(panels[pan].container) )
    {
      panels[pan].container.style.display = '';
      return true;
    }
    // Otherwise make sure it's been removed from the framework
    else
    {
      panels[pan].container.style.display='none';
      return false;
    }
  }

  if ( panel_is_alive('left') )
  {
    col_span += 1;      
  }

//  if ( panel_is_alive('top') )
//  {
    // NOP
//  }

  if ( panel_is_alive('right') )
  {
    col_span += 1;
  }

//  if ( panel_is_alive('bottom') )
//  {
    // NOP
//  }

  framework.tb_cell.colSpan = col_span;
  framework.tp_cell.colSpan = col_span;
  framework.bp_cell.colSpan = col_span;
  framework.sb_cell.colSpan = col_span;

  // Put in the panel rows, top panel goes above editor row
  if ( !framework.tp_row.childNodes.length )
  {
    Xinha.removeFromParent(framework.tp_row);
  }
  else
  {
    if ( !Xinha.hasParentNode(framework.tp_row) )
    {
      framework.tbody.insertBefore(framework.tp_row, framework.ler_row);
    }
  }

  // bp goes after the editor
  if ( !framework.bp_row.childNodes.length )
  {
    Xinha.removeFromParent(framework.bp_row);
  }
  else
  {
    if ( !Xinha.hasParentNode(framework.bp_row) )
    {
      framework.tbody.insertBefore(framework.bp_row, framework.ler_row.nextSibling);
    }
  }

  // finally if the statusbar is on, insert it
  if ( !this.config.statusBar )
  {
    Xinha.removeFromParent(framework.sb_row);
  }
  else
  {
    if ( !Xinha.hasParentNode(framework.sb_row) )
    {
      framework.table.appendChild(framework.sb_row);
    }
  }

  // Size and set colspans, link up the framework
  framework.lp_cell.style.width  = this.config.panel_dimensions.left;
  framework.rp_cell.style.width  = this.config.panel_dimensions.right;
  framework.tp_cell.style.height = this.config.panel_dimensions.top;
  framework.bp_cell.style.height = this.config.panel_dimensions.bottom;
  framework.tb_cell.style.height = this._toolBar.offsetHeight + 'px';
  framework.sb_cell.style.height = this._statusBar.offsetHeight + 'px';

  var edcellheight = height - this._toolBar.offsetHeight - this._statusBar.offsetHeight;
  if ( panel_is_alive('top') )
  {
    edcellheight -= parseInt(this.config.panel_dimensions.top, 10);
  }
  if ( panel_is_alive('bottom') )
  {
    edcellheight -= parseInt(this.config.panel_dimensions.bottom, 10);
  }
  this._iframe.style.height = edcellheight + 'px';  
  
  var edcellwidth = width;
  if ( panel_is_alive('left') )
  {
    edcellwidth -= parseInt(this.config.panel_dimensions.left, 10);
  }
  if ( panel_is_alive('right') )
  {
    edcellwidth -= parseInt(this.config.panel_dimensions.right, 10);    
  }
  var iframeWidth = this.config.iframeWidth ? parseInt(this.config.iframeWidth,10) : null; 
  this._iframe.style.width = (iframeWidth && iframeWidth < edcellwidth) ? iframeWidth + "px": edcellwidth + "px"; 

  this._textArea.style.height = this._iframe.style.height;
  this._textArea.style.width  = this._iframe.style.width;
     
  this.notifyOf('resize', {width:this._htmlArea.offsetWidth, height:this._htmlArea.offsetHeight});
  this.firePluginEvent('onResize',this._htmlArea.offsetWidth, this._htmlArea.offsetWidth);
  this._risizing = false;
};
/** FIXME: Never used, what is this for? 
* @param {string} side 
* @param {Object}
*/
Xinha.prototype.registerPanel = function(side, object)
{
  if ( !side )
  {
    side = 'right';
  }
  this.setLoadingMessage('Register ' + side + ' panel ');
  var panel = this.addPanel(side);
  if ( object )
  {
    object.drawPanelIn(panel);
  }
};
/** Creates a panel in the panel container on the specified side
* @param {String} side the panel container to which the new panel will be added<br />
*									Possible values are: "right","left","top","bottom"
* @returns {DomNode} Panel div
*/
Xinha.prototype.addPanel = function(side)
{
  var div = document.createElement('div');
  div.side = side;
  if ( side == 'left' || side == 'right' )
  {
    div.style.width  = this.config.panel_dimensions[side];
    if (this._iframe) 
    {
      div.style.height = this._iframe.style.height;
    }
  }
  Xinha.addClasses(div, 'panel');
  this._panels[side].panels.push(div);
  this._panels[side].div.appendChild(div);

  this.notifyOf('panel_change', {'action':'add','panel':div});
  this.firePluginEvent('onPanelChange','add',div);
  return div;
};
/** Removes a panel
* @param {DomNode} panel object as returned by Xinha.prototype.addPanel()
*/
Xinha.prototype.removePanel = function(panel)
{
  this._panels[panel.side].div.removeChild(panel);
  var clean = [];
  for ( var i = 0; i < this._panels[panel.side].panels.length; i++ )
  {
    if ( this._panels[panel.side].panels[i] != panel )
    {
      clean.push(this._panels[panel.side].panels[i]);
    }
  }
  this._panels[panel.side].panels = clean;
  this.notifyOf('panel_change', {'action':'remove','panel':panel});
  this.firePluginEvent('onPanelChange','remove',panel);
};
/** Hides a panel
* @param {DomNode} panel object as returned by Xinha.prototype.addPanel()
*/
Xinha.prototype.hidePanel = function(panel)
{
  if ( panel && panel.style.display != 'none' )
  {
    try { var pos = this.scrollPos(this._iframe.contentWindow); } catch(e) { }
    panel.style.display = 'none';
    this.notifyOf('panel_change', {'action':'hide','panel':panel});
    this.firePluginEvent('onPanelChange','hide',panel);
    try { this._iframe.contentWindow.scrollTo(pos.x,pos.y); } catch(e) { }
  }
};
/** Shows a panel
* @param {DomNode} panel object as returned by Xinha.prototype.addPanel()
*/
Xinha.prototype.showPanel = function(panel)
{
  if ( panel && panel.style.display == 'none' )
  {
    try { var pos = this.scrollPos(this._iframe.contentWindow); } catch(e) {}
    panel.style.display = '';
    this.notifyOf('panel_change', {'action':'show','panel':panel});
    this.firePluginEvent('onPanelChange','show',panel);
    try { this._iframe.contentWindow.scrollTo(pos.x,pos.y); } catch(e) { }
  }
};
/** Hides the panel(s) on one or more sides
* @param {Array} sides the sides on which the panels shall be hidden
*/
Xinha.prototype.hidePanels = function(sides)
{
  if ( typeof sides == 'undefined' )
  {
    sides = ['left','right','top','bottom'];
  }

  var reShow = [];
  for ( var i = 0; i < sides.length;i++ )
  {
    if ( this._panels[sides[i]].on )
    {
      reShow.push(sides[i]);
      this._panels[sides[i]].on = false;
    }
  }
  this.notifyOf('panel_change', {'action':'multi_hide','sides':sides});
  this.firePluginEvent('onPanelChange','multi_hide',sides);
};
/** Shows the panel(s) on one or more sides
* @param {Array} sides the sides on which the panels shall be hidden
*/
Xinha.prototype.showPanels = function(sides)
{
  if ( typeof sides == 'undefined' )
  {
    sides = ['left','right','top','bottom'];
  }

  var reHide = [];
  for ( var i = 0; i < sides.length; i++ )
  {
    if ( !this._panels[sides[i]].on )
    {
      reHide.push(sides[i]);
      this._panels[sides[i]].on = true;
    }
  }
  this.notifyOf('panel_change', {'action':'multi_show','sides':sides});
  this.firePluginEvent('onPanelChange','multi_show',sides);
};
/** Returns an array containig all properties that are set in an object
* @param {Object} obj
* @returns {Array}
*/
Xinha.objectProperties = function(obj)
{
  var props = [];
  for ( var x in obj )
  {
    props[props.length] = x;
  }
  return props;
};

/** Checks if editor is active
 *<br />
 * EDITOR ACTIVATION NOTES:<br />
 *  when a page has multiple Xinha editors, ONLY ONE should be activated at any time (this is mostly to
 *  work around a bug in Mozilla, but also makes some sense).  No editor should be activated or focused
 *  automatically until at least one editor has been activated through user action (by mouse-clicking in
 *  the editor).
 * @private
 * @returns {Boolean}
 */
Xinha.prototype.editorIsActivated = function()
{
  try
  {
    return Xinha.is_designMode ? this._doc.designMode == 'on' : this._doc.body.contentEditable;
  }
  catch (ex)
  {
    return false;
  }
};
/**  We need to know that at least one editor on the page has been activated
*    this is because we will not focus any editor until an editor has been activated
* @private
* @type {Boolean}
*/
Xinha._someEditorHasBeenActivated = false;
/**  Stores a reference to the currently active editor
* @private
* @type {Xinha}
*/
Xinha._currentlyActiveEditor      = null;
/** Enables one editor for editing, e.g. by a click in the editing area or after it has been 
 *  deactivated programmatically before 
 * @private
 * @returns {Boolean}
 */
Xinha.prototype.activateEditor = function()
{
  if (this.currentModal) 
  {
    return;
  }
  // We only want ONE editor at a time to be active
  if ( Xinha._currentlyActiveEditor )
  {
    if ( Xinha._currentlyActiveEditor == this )
    {
      return true;
    }
    Xinha._currentlyActiveEditor.deactivateEditor();
  }

  if ( Xinha.is_designMode && this._doc.designMode != 'on' )
  {
    try
    {
      // cannot set design mode if no display
      if ( this._iframe.style.display == 'none' )
      {
        this._iframe.style.display = '';
        this._doc.designMode = 'on';
        this._iframe.style.display = 'none';
      }
      else
      {
        this._doc.designMode = 'on';
      }

      // Opera loses some of it's event listeners when the designMode is set to on.
	  // the true just shortcuts the method to only set some listeners.
      if(Xinha.is_opera) this.setEditorEvents(true);

    } catch (ex) {}
  }
  else if ( Xinha.is_ie&& this._doc.body.contentEditable !== true )
  {
    this._doc.body.contentEditable = true;
  }

  Xinha._someEditorHasBeenActivated = true;
  Xinha._currentlyActiveEditor      = this;

  var editor = this;
  this.enableToolbar();
};
/** Disables the editor 
 * @private
 */
Xinha.prototype.deactivateEditor = function()
{
  // If the editor isn't active then the user shouldn't use the toolbar
  this.disableToolbar();

  if ( Xinha.is_designMode && this._doc.designMode != 'off' )
  {
    try
    {
      this._doc.designMode = 'off';
    } catch (ex) {}
  }
  else if ( !Xinha.is_designMode && this._doc.body.contentEditable !== false )
  {
    this._doc.body.contentEditable = false;
  }

  if ( Xinha._currentlyActiveEditor != this )
  {
    // We just deactivated an editor that wasn't marked as the currentlyActiveEditor

    return; // I think this should really be an error, there shouldn't be a situation where
            // an editor is deactivated without first being activated.  but it probably won't
            // hurt anything.
  }

  Xinha._currentlyActiveEditor = false;
};
/** Creates the iframe (editable area)
 * @private
 */
Xinha.prototype.initIframe = function()
{
  this.disableToolbar();
  var doc = null;
  var editor = this;
  try
  {
    if ( editor._iframe.contentDocument )
    {
      this._doc = editor._iframe.contentDocument;        
    }
    else
    {
      this._doc = editor._iframe.contentWindow.document;
    }
    doc = this._doc;
    // try later
    if ( !doc )
    {
      if ( Xinha.is_gecko )
      {
        setTimeout(function() { editor.initIframe(); }, 50);
        return false;
      }
      else
      {
        alert("ERROR: IFRAME can't be initialized.");
      }
    }
  }
  catch(ex)
  { // try later
    setTimeout(function() { editor.initIframe(); }, 50);
    return false;
  }
  
  Xinha.freeLater(this, '_doc');

  doc.open("text/html","replace");
  var html = '', doctype;
  if ( editor.config.browserQuirksMode === false )
  {
    doctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">';
  }
  else if ( editor.config.browserQuirksMode === true )
  {
    doctype = '';
  }
  else
  {
    doctype = Xinha.getDoctype(document);
  }
  
  if ( !editor.config.fullPage )
  {
    html += doctype + "\n";
    html += "<html>\n";
    html += "<head>\n";
    html += "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=" + editor.config.charSet + "\">\n";
    if ( typeof editor.config.baseHref != 'undefined' && editor.config.baseHref !== null )
    {
      html += "<base href=\"" + editor.config.baseHref + "\"/>\n";
    }
    
    html += Xinha.addCoreCSS();
    
    if ( typeof editor.config.pageStyleSheets !== 'undefined' )
    {
      for ( var i = 0; i < editor.config.pageStyleSheets.length; i++ )
      {
        if ( editor.config.pageStyleSheets[i].length > 0 )
        {
          html += "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + editor.config.pageStyleSheets[i] + "\">";
          //html += "<style> @import url('" + editor.config.pageStyleSheets[i] + "'); </style>\n";
        }
      }
    }
    
    if ( editor.config.pageStyle )
    {
      html += "<style type=\"text/css\">\n" + editor.config.pageStyle + "\n</style>";
    }
    
    html += "</head>\n";
    html += "<body" + (editor.config.bodyID ? (" id=\"" + editor.config.bodyID + "\"") : '') + (editor.config.bodyClass ? (" class=\"" + editor.config.bodyClass + "\"") : '') + ">\n";
    html +=   editor.inwardHtml(editor._textArea.value);
    html += "</body>\n";
    html += "</html>";
  }
  else
  {
    html = editor.inwardHtml(editor._textArea.value);
    if ( html.match(Xinha.RE_doctype) )
    {
      editor.setDoctype(RegExp.$1);
      //html = html.replace(Xinha.RE_doctype, "");
    }
    
    //Fix Firefox problem with link elements not in right place (just before head)
    var match = html.match(/<link\s+[\s\S]*?["']\s*\/?>/gi);
    html = html.replace(/<link\s+[\s\S]*?["']\s*\/?>\s*/gi, '');
    if (match)
    {
      html = html.replace(/<\/head>/i, match.join('\n') + "\n</head>");
    }
  }
  doc.write(html);
  doc.close();
  if ( this.config.fullScreen )
  {
    this._fullScreen();
  }
  this.setEditorEvents();


  // If this IFRAME had been configured for autofocus, we'll focus it now,
  // since everything needed to do so is now fully loaded.
  if ((typeof editor.config.autofocus != "undefined") && editor.config.autofocus !== false &&
      ((editor.config.autofocus == editor._textArea.id) || editor.config.autofocus == true))
  {
    editor.activateEditor();
    editor.focusEditor();
  }
};
  
/**
 * Delay a function until the document is ready for operations.
 * See ticket:547
 * @public
 * @param {Function} f  The function to call once the document is ready
 */
Xinha.prototype.whenDocReady = function(f)
{
  var e = this;
  if ( this._doc && this._doc.body )
  {
    f();
  }
  else
  {
    setTimeout(function() { e.whenDocReady(f); }, 50);
  }
};


/** Switches editor mode between wysiwyg and text (HTML)
 * @param {String} mode optional "textmode" or "wysiwyg", if omitted, toggles between modes.
 */
Xinha.prototype.setMode = function(mode)
{
  var html;
  if ( typeof mode == "undefined" )
  {
    mode = this._editMode == "textmode" ? "wysiwyg" : "textmode";
  }
  switch ( mode )
  {
    case "textmode":
      this.firePluginEvent('onBeforeMode', 'textmode');
      this._toolbarObjects.htmlmode.swapImage(this.config.iconList.wysiwygmode); 
      this.setCC("iframe");
      html = this.outwardHtml(this.getHTML());
      this.setHTML(html);

      // Hide the iframe
      this.deactivateEditor();
      this._iframe.style.display   = 'none';
      this._textArea.style.display = '';

      if ( this.config.statusBar )
      {
        this._statusBarTree.style.display = "none";
        this._statusBarTextMode.style.display = "";
      }
      this.findCC("textarea");
      this.notifyOf('modechange', {'mode':'text'});
      this.firePluginEvent('onMode', 'textmode');
    break;

    case "wysiwyg":
      this.firePluginEvent('onBeforeMode', 'wysiwyg');
      this._toolbarObjects.htmlmode.swapImage([this.imgURL('images/ed_buttons_main.png'),7,0]); 
      this.setCC("textarea");
      html = this.inwardHtml(this.getHTML());
      this.deactivateEditor();
      this.setHTML(html);
      this._iframe.style.display   = '';
      this._textArea.style.display = "none";
      this.activateEditor();
      if ( this.config.statusBar )
      {
        this._statusBarTree.style.display = "";
        this._statusBarTextMode.style.display = "none";
      }
      this.findCC("iframe");
      this.notifyOf('modechange', {'mode':'wysiwyg'});
      this.firePluginEvent('onMode', 'wysiwyg');

    break;

    default:
      alert("Mode <" + mode + "> not defined!");
      return false;
  }
  this._editMode = mode;
};
/** Sets the HTML in fullpage mode. Actually the whole iframe document is rewritten.
 * @private
 * @param {String} html
 */
Xinha.prototype.setFullHTML = function(html)
{
  var save_multiline = RegExp.multiline;
  RegExp.multiline = true;
  if ( html.match(Xinha.RE_doctype) )
  {
    this.setDoctype(RegExp.$1);
   // html = html.replace(Xinha.RE_doctype, "");
  }
  RegExp.multiline = save_multiline;
  // disabled to save body attributes see #459
  if ( 0 )
  {
    if ( html.match(Xinha.RE_head) )
    {
      this._doc.getElementsByTagName("head")[0].innerHTML = RegExp.$1;
    }
    if ( html.match(Xinha.RE_body) )
    {
      this._doc.getElementsByTagName("body")[0].innerHTML = RegExp.$1;
    }
  }
  else
  {
    // FIXME - can we do this without rewriting the entire document
    //  does the above not work for IE?
    var reac = this.editorIsActivated();
    if ( reac )
    {
      this.deactivateEditor();
    }
    var html_re = /<html>((.|\n)*?)<\/html>/i;
    html = html.replace(html_re, "$1");
    this._doc.open("text/html","replace");
    this._doc.write(html);
    this._doc.close();
    if ( reac )
    {
      this.activateEditor();
    }        
    this.setEditorEvents();
    return true;
  }
};
/** Initialize some event handlers
 * @private
 */
Xinha.prototype.setEditorEvents = function(resetting_events_for_opera)
{
  var editor=this;
  var doc = this._doc;

  editor.whenDocReady(
    function()
    {
      if(!resetting_events_for_opera) {
      // if we have multiple editors some bug in Mozilla makes some lose editing ability
      Xinha._addEvents(
        doc,
        ["mousedown"],
        function()
        {
          editor.activateEditor();
          return true;
        }
      );
      if (Xinha.is_ie)
      { // #1019 Cusor not jumping to editable part of window when clicked in IE, see also #1039
        Xinha._addEvent(
        editor._doc.getElementsByTagName("html")[0],
        "click",
          function()
          {
            if (editor._iframe.contentWindow.event.srcElement.tagName.toLowerCase() == 'html') // if  clicked below the text (=body), the text cursor does not appear, see #1019
            {
               var r = editor._doc.body.createTextRange();
               r.collapse();
               r.select();
               //setTimeout (function () { r.collapse();  r.select();},100); // won't do without timeout, dunno why
             }
             return true;
          }
        );
      }
      }

      // intercept some events; for updating the toolbar & keyboard handlers
      Xinha._addEvents(
        doc,
        ["keydown", "keypress", "mousedown", "mouseup", "drag"],
        function (event)
        {
          return editor._editorEvent(Xinha.is_ie ? editor._iframe.contentWindow.event : event);
        }
      );
      
      Xinha._addEvents(
        doc, 
        ["dblclick"],
        function (event)
        {
          return editor._onDoubleClick(Xinha.is_ie ? editor._iframe.contentWindow.event : event);
        }
      );
      
      if(resetting_events_for_opera) return;

      // FIXME - this needs to be cleaned up and use editor.firePluginEvent
      //  I don't like both onGenerate and onGenerateOnce, we should only
      //  have onGenerate and it should only be called when the editor is 
      //  generated (once and only once)
      // check if any plugins have registered refresh handlers
      for ( var i in editor.plugins )
      {
        var plugin = editor.plugins[i].instance;
        Xinha.refreshPlugin(plugin);
      }

      // specific editor initialization
      if ( typeof editor._onGenerate == "function" )
      {
        editor._onGenerate();
      }
      
      if(Xinha.hasAttribute(editor._textArea, 'onxinhaready'))
      {               
        (function() { eval(editor._textArea.getAttribute('onxinhaready')) }).call(editor.textArea);
      }
      
      //ticket #1407 IE8 fires two resize events on one actual resize, seemingly causing an infinite loop (but not  when Xinha is in an frame/iframe) 
      Xinha.addDom0Event(window, 'resize', function(e) 
      {
        if (Xinha.ie_version > 7 && !window.parent)
        {
          if (editor.execResize)
          {
            editor.sizeEditor(); 
            editor.execResize = false;
          }
          else
          {
            editor.execResize = true;
          }
        }
        else
        {
          editor.sizeEditor(); 
        }
      });      
      editor.removeLoadingMessage();
    }
  );
};
  
/***************************************************
 *  Category: PLUGINS
 ***************************************************/
/** Plugins may either reside in the golbal scope (not recommended) or in Xinha.plugins. 
 *  This function looks in both locations and is used to check the loading status and finally retrieve the plugin's constructor
 * @private
 * @type {Function|undefined}
 * @param {String} pluginName
 */
Xinha.getPluginConstructor = function(pluginName)
{
  return Xinha.plugins[pluginName] || window[pluginName];
};

/** Create the specified plugin and register it with this Xinha
 *  return the plugin created to allow refresh when necessary.<br />
 *  <strong>This is only useful if Xinha is generated without using Xinha.makeEditors()</strong>
 */
Xinha.prototype.registerPlugin = function()
{
  if (!Xinha.isSupportedBrowser)
  {
    return;
  }
  var plugin = arguments[0];

  // We can only register plugins that have been succesfully loaded
  if ( plugin === null || typeof plugin == 'undefined' || (typeof plugin == 'string' && Xinha.getPluginConstructor(plugin) == 'undefined') )
  {
    return false;
  }
  var args = [];
  for ( var i = 1; i < arguments.length; ++i )
  {
    args.push(arguments[i]);
  }
  return this.registerPlugin2(plugin, args);
};
/** This is the variant of the function above where the plugin arguments are
 * already packed in an array.  Externally, it should be only used in the
 * full-screen editor code, in order to initialize plugins with the same
 * parameters as in the opener window.
 * @private
 */
Xinha.prototype.registerPlugin2 = function(plugin, args)
{
  if ( typeof plugin == "string" && typeof Xinha.getPluginConstructor(plugin) == 'function' )
  {
    var pluginName = plugin;
    plugin = Xinha.getPluginConstructor(plugin);
  }
  if ( typeof plugin == "undefined" )
  {
    /* FIXME: This should never happen. But why does it do? */
    return false;
  }
  if (!plugin._pluginInfo) 
  {
    plugin._pluginInfo = 
    {
      name: pluginName
    };
  }
  var obj = new plugin(this, args);
  if ( obj )
  {
    var clone = {};
    var info = plugin._pluginInfo;
    for ( var i in info )
    {
      clone[i] = info[i];
    }
    clone.instance = obj;
    clone.args = args;
    this.plugins[plugin._pluginInfo.name] = clone;
    return obj;
  }
  else
  {
    Xinha.debugMsg("Can't register plugin " + plugin.toString() + ".", 'warn');
  }
};


/** Dynamically returns the directory from which the plugins are loaded<br />
 *  This could be overridden to change the dir<br />
 *  @TODO: Wouldn't this be better as a config option?
 * @private
 * @param {String} pluginName
 * @param {Boolean} return the directory for an unsupported plugin
 * @returns {String} path to plugin
 */
Xinha.getPluginDir = function(plugin, forceUnsupported)
{
  if (Xinha.externalPlugins[plugin])
  {
    return Xinha.externalPlugins[plugin][0];
  }
  if (forceUnsupported ||
      // If the plugin is fully loaded, it's supported status is already set.
      (Xinha.getPluginConstructor(plugin) && (typeof Xinha.getPluginConstructor(plugin).supported != 'undefined') && !Xinha.getPluginConstructor(plugin).supported))
  {
    return _editor_url + "unsupported_plugins/" + plugin ;
  }
  return _editor_url + "plugins/" + plugin ;
};
/** Static function that loads the given plugin
 * @param {String} pluginName
 * @param {Function} callback function to be called when file is loaded
 * @param {String} plugin_file URL of the file to load
 * @returns {Boolean} true if plugin loaded, false otherwise
 */
Xinha.loadPlugin = function(pluginName, callback, url)
{
  if (!Xinha.isSupportedBrowser) 
  {
    return;
  }
  Xinha.setLoadingMessage (Xinha._lc("Loading plugin $plugin="+pluginName+"$"));

  // Might already be loaded
  if ( typeof Xinha.getPluginConstructor(pluginName) != 'undefined' )
  {
    if ( callback )
    {
      callback(pluginName);
    }
    return true;
  }
  Xinha._pluginLoadStatus[pluginName] = 'loading';

  /** This function will try to load a plugin in multiple passes.  It tries to
   * load the plugin from either the plugin or unsupported directory, using
   * both naming schemes in this order:
   * 1. /plugins -> CurrentNamingScheme
   * 2. /plugins -> old-naming-scheme
   * 3. /unsupported -> CurrentNamingScheme
   * 4. /unsupported -> old-naming-scheme
   */
  function multiStageLoader(stage,pluginName)
  {
    var nextstage, dir, file, success_message;
    switch (stage)
    {
      case 'start':
        nextstage = 'old_naming';
        dir = Xinha.getPluginDir(pluginName);
        file = pluginName + ".js";
        break;
      case 'old_naming':
        nextstage = 'unsupported';
        dir = Xinha.getPluginDir(pluginName);
        file = pluginName.replace(/([a-z])([A-Z])([a-z])/g, function (str, l1, l2, l3) { return l1 + "-" + l2.toLowerCase() + l3; }).toLowerCase() + ".js";
        success_message = 'You are using an obsolete naming scheme for the Xinha plugin '+pluginName+'. Please rename '+file+' to '+pluginName+'.js';
        break;
      case 'unsupported':
        nextstage = 'unsupported_old_name';
        dir = Xinha.getPluginDir(pluginName, true);
        file = pluginName + ".js";
        success_message = 'You are using the unsupported Xinha plugin '+pluginName+'. If you wish continued support, please see http://trac.xinha.org/wiki/Documentation/UnsupportedPlugins';
        break;
      case 'unsupported_old_name':
        nextstage = '';
        dir = Xinha.getPluginDir(pluginName, true);
        file = pluginName.replace(/([a-z])([A-Z])([a-z])/g, function (str, l1, l2, l3) { return l1 + "-" + l2.toLowerCase() + l3; }).toLowerCase() + ".js";
        success_message = 'You are using the unsupported Xinha plugin '+pluginName+'. If you wish continued support, please see http://trac.xinha.org/wiki/Documentation/UnsupportedPlugins';
        break;
      default:
        Xinha._pluginLoadStatus[pluginName] = 'failed';
        Xinha.debugMsg('Xinha was not able to find the plugin '+pluginName+'. Please make sure the plugin exists.', 'warn');
        return;
    }
    var url = dir + "/" + file;

    // This is a callback wrapper that allows us to set the plugin's status
    // once it loads.
    function statusCallback(pluginName)
    {
      Xinha.getPluginConstructor(pluginName).supported = stage.indexOf('unsupported') !== 0;
      callback(pluginName);
    }

    // To speed things up, we start loading the script file before pinging it.
    // If the load fails, we'll just clean up afterwards.
    Xinha._loadback(url, statusCallback, this, pluginName); 

    Xinha.ping(url,
               // On success, we'll display a success message if there is one.
               function()
               {
                 if (success_message) 
                 {
                   Xinha.debugMsg(success_message);
                 }
               },
               // On failure, we'll clean up the failed load and try the next stage
               function()
               {
                 Xinha.removeFromParent(document.getElementById(url));
                 multiStageLoader(nextstage, pluginName);
               });
  }
  
  if(!url)
  {
    if (Xinha.externalPlugins[pluginName])
    {
      Xinha._loadback(Xinha.externalPlugins[pluginName][0]+Xinha.externalPlugins[pluginName][1], callback, this, pluginName);
    }
    else
    {
      var editor = this;
      multiStageLoader('start',pluginName);
    }
  }
  else
  {
    Xinha._loadback(url, callback, this, pluginName);
  }
  
  return false;
};
/** Stores a status for each loading plugin that may be one of "loading","ready", or "failed"
 * @private
 * @type {Object} 
 */
Xinha._pluginLoadStatus = {};
/** Stores the paths to plugins that are not in the default location
 * @private
 * @type {Object}
 */
Xinha.externalPlugins = {};
/** The namespace for plugins
 * @private
 * @type {Object}
 */
Xinha.plugins = {};

/** Static function that loads the plugins (see xinha_plugins in NewbieGuide)
 * @param {Array} plugins
 * @param {Function} callbackIfNotReady function that is called repeatedly until all files are
 * @param {String} optional url URL of the plugin file; obviously plugins should contain only one item if url is given
 * @returns {Boolean} true if all plugins are loaded, false otherwise
 */
Xinha.loadPlugins = function(plugins, callbackIfNotReady,url)
{
  if (!Xinha.isSupportedBrowser) 
  {
    return;
  }
  //Xinha.setLoadingMessage (Xinha._lc("Loading plugins"));
  var m,i;
  for (i=0;i<plugins.length;i++)
  {
    if (typeof plugins[i] == 'object')
    {
      m = plugins[i].url.match(/(.*)(\/[^\/]*)$/);
      Xinha.externalPlugins[plugins[i].plugin] = [m[1],m[2]];
      plugins[i] = plugins[i].plugin;
    }
  }
  
  // Rip the ones that are loaded and look for ones that have failed
  var retVal = true;
  var nuPlugins = Xinha.cloneObject(plugins);
  for (i=0;i<nuPlugins.length;i++ )
  {
    var p = nuPlugins[i];
    
    if (p == 'FullScreen' && !Xinha.externalPlugins.FullScreen)
    {
      continue; //prevent trying to load FullScreen plugin from the plugins folder
    } 
   
    if ( typeof Xinha._pluginLoadStatus[p] == 'undefined')
    {
      // Load it
      Xinha.loadPlugin(p,
        function(plugin)
        {
          Xinha.setLoadingMessage (Xinha._lc("Finishing"));

          if ( typeof Xinha.getPluginConstructor(plugin) != 'undefined' )
          {
            Xinha._pluginLoadStatus[plugin] = 'ready';
          }
          else
          {
            Xinha._pluginLoadStatus[plugin] = 'failed';
          }
        }, url);
      retVal = false;
    }
    else if ( Xinha._pluginLoadStatus[p] == 'loading')
    {
      retVal = false;
    }
  }
  
  // All done, just return
  if ( retVal )
  {
    return true;
  }

  // Waiting on plugins to load, return false now and come back a bit later
  // if we have to callback
  if ( callbackIfNotReady )
  {
    setTimeout(function() 
    { 
      if ( Xinha.loadPlugins(plugins, callbackIfNotReady) ) 
      { 
        callbackIfNotReady(); 
      } 
    }, 50);
  }
  return retVal;
};

// 
/** Refresh plugin by calling onGenerate or onGenerateOnce method.
 * @private
 * @param {PluginInstance} plugin
 */
Xinha.refreshPlugin = function(plugin)
{
  if ( plugin && typeof plugin.onGenerate == "function" )
  {
    plugin.onGenerate();
  }
  if ( plugin && typeof plugin.onGenerateOnce == "function" )
  {
    //#1392: in fullpage mode this function is called recusively by setFullHTML() when it is used to set the editor content
	// this is a temporary fix, that should better be handled by a better implemetation of setFullHTML
	plugin._ongenerateOnce = plugin.onGenerateOnce;
    delete(plugin.onGenerateOnce);
	plugin._ongenerateOnce();
	delete(plugin._ongenerateOnce);
  }
};

/** Call a method of all plugins which define the method using the supplied arguments.<br /><br />
 *
 *  Example: <code>editor.firePluginEvent('onExecCommand', 'paste')</code><br />
 *           The plugin would then define a method<br />
 *           <code>PluginName.prototype.onExecCommand = function (cmdID, UI, param) {do something...}</code><br /><br />
 *           The following methodNames are currently available:<br />
 *  <table border="1">
 *    <tr>
 *       <th>methodName</th><th>Parameters</th>
 *     </tr>
 *     <tr>
 *       <td>onExecCommand</td><td> cmdID, UI, param</td>
 *     </tr>
 *     <tr>
 *       <td>onKeyPress</td><td>ev</td>
 *     </tr> 
 *     <tr>
 *       <td>onMouseDown</td><td>ev</td>
 *     </tr>
 * </table><br /><br />
 *  
 *  The browser specific plugin (if any) is called last.  The result of each call is 
 *  treated as boolean.  A true return means that the event will stop, no further plugins
 *  will get the event, a false return means the event will continue to fire.
 *
 *  @param {String} methodName
 *  @param {mixed} arguments to pass to the method, optional [2..n] 
 *  @returns {Boolean}
 */

Xinha.prototype.firePluginEvent = function(methodName)
{
  // arguments is not a real array so we can't just .shift() it unfortunatly.
  var argsArray = [ ];
  for(var i = 1; i < arguments.length; i++)
  {
    argsArray[i-1] = arguments[i];
  }
  
  for ( i in this.plugins )
  {
    var plugin = this.plugins[i].instance;

    // Skip the browser specific plugin
    if (plugin == this._browserSpecificPlugin) 
    {
      continue;
    }
    if ( plugin && typeof plugin[methodName] == "function" )
    {
      var thisArg = (i == 'Events') ? this : plugin;
      if ( plugin[methodName].apply(thisArg, argsArray) )
      {
        return true;
      }
    }
  }
  
  // Now the browser speific
  plugin = this._browserSpecificPlugin;
  if ( plugin && typeof plugin[methodName] == "function" )
  {
    if ( plugin[methodName].apply(plugin, argsArray) )
    {
      return true;
    }
  }
  return false;
};
/** Adds a stylesheet to the document
 * @param {String} style name of the stylesheet file
 * @param {String} plugin optional name of a plugin; if passed this function looks for the stylesheet file in the plugin directory 
 * @param {String} id optional a unique id for identifiing the created link element, e.g. for avoiding double loading 
 *                 or later removing it again
 */
Xinha.loadStyle = function(style, plugin, id,prepend)
{
  var url = _editor_url || '';
  if ( plugin )
  {
    url = Xinha.getPluginDir( plugin ) + "/";
  }
  url += style;
  // @todo: would not it be better to check the first character instead of a regex ?
  // if ( typeof style == 'string' && style.charAt(0) == '/' )
  // {
  //   url = style;
  // }
  if ( /^\//.test(style) )
  {
    url = style;
  }
  var head = document.getElementsByTagName("head")[0];
  var link = document.createElement("link");
  link.rel = "stylesheet";
  link.href = url;
  link.type = "text/css";
  if (id)
  {
    link.id = id;
  }
  if (prepend && head.getElementsByTagName('link')[0])
  {
    head.insertBefore(link,head.getElementsByTagName('link')[0]);
  }
  else
  {
    head.appendChild(link);
  }
  
};

/** Adds a script to the document
 *
 * Warning: Browsers may cause the script to load asynchronously.
 *
 * @param {String} style name of the javascript file
 * @param {String} plugin optional name of a plugin; if passed this function looks for the stylesheet file in the plugin directory 
 *
 */
Xinha.loadScript = function(script, plugin, callback)
{
  var url = _editor_url || '';
  if ( plugin )
  {
    url = Xinha.getPluginDir( plugin ) + "/";
  }
  url += script;
  // @todo: would not it be better to check the first character instead of a regex ?
  // if ( typeof style == 'string' && style.charAt(0) == '/' )
  // {
  //   url = style;
  // }
  if ( /^\//.test(script) )
  {
    url = script;
  }
  
  Xinha._loadback(url, callback);
  
};

/** Load one or more assets, sequentially, where an asset is a CSS file, or a javascript file.
 *  
 * Example Usage:
 *
 * Xinha.includeAssets( 'foo.css', 'bar.js', [ 'foo.css', 'MyPlugin' ], { type: 'text/css', url: 'foo.php', plugin: 'MyPlugin } );
 *
 * Alternative usage, use Xinha.includeAssets() to make a loader, then use loadScript, loadStyle and whenReady methods
 * on your loader object as and when you wish, you can chain the calls if you like.
 *
 * You may add any number of callbacks using .whenReady() multiple times.
 *
 *   var myAssetLoader = Xinha.includeAssets();
 *       myAssetLoader.loadScript('foo.js', 'MyPlugin')
 *                    .loadStyle('foo.css', 'MyPlugin');                        
 * 
 */

Xinha.includeAssets = function()
{
  var assetLoader = { pendingAssets: [ ], loaderRunning: false, loadedScripts: [ ] };
  
  assetLoader.callbacks = [ ];
  
  assetLoader.loadNext = function()
  {  
    var self = this;
    this.loaderRunning = true;
    
    if(this.pendingAssets.length)
    {
      var nxt = this.pendingAssets[0];
      this.pendingAssets.splice(0,1); // Remove 1 element
      switch(nxt.type)
      {
        case 'text/css':
          Xinha.loadStyle(nxt.url, nxt.plugin);
          return this.loadNext();
        
        case 'text/javascript':          
          this.loadedScripts.push(nxt);
          Xinha.loadScript(nxt.url, nxt.plugin, function() { self.loadNext(); });
      }
    }
    else
    {
      this.loaderRunning = false;
      this.runCallback();      
    }
  };
  
  assetLoader.loadScript = function(url, plugin)
  {
    var self = this;
    
    this.pendingAssets.push({ 'type': 'text/javascript', 'url': url, 'plugin': plugin });
    if(!this.loaderRunning) this.loadNext();
    
    return this;
  };
  
  assetLoader.loadScriptOnce = function(url, plugin)
  {
    for(var i = 0; i < this.loadedScripts.length; i++)
    {
      if(this.loadedScripts[i].url == url && this.loadedScripts[i].plugin == plugin)
      {
        if(!this.loaderRunning) this.loadNext();
        return this; // Already done (or in process)
      }
    }
    
    for(var i = 0; i < this.pendingAssets.length; i++)
    {
      if(this.pendingAssets[i].url == url && this.pendingAssets[i].plugin == plugin)
      {
        if(!this.loaderRunning) this.loadNext();
        return this; // Already pending
      }
    }
        
    return this.loadScript(url, plugin);
  }
  
  assetLoader.loadStyle = function(url, plugin)
  {
    var self = this;
    
    this.pendingAssets.push({ 'type': 'text/css', 'url': url, 'plugin': plugin });
    if(!this.loaderRunning) this.loadNext();
    
    return this;    
  };
  
  assetLoader.whenReady = function(callback) 
  {
    this.callbacks.push(callback);    
    if(!this.loaderRunning) this.loadNext();
    
    return this;    
  };
  
  assetLoader.runCallback = function()
  {
    while(this.callbacks.length)
    { 
      var _callback = this.callbacks.splice(0,1);
      _callback[0]();
      _callback = null;
    }
    return this;
  }
  
  for(var i = 0 ; i < arguments.length; i++)
  {
    if(typeof arguments[i] == 'string')
    {
      if(arguments[i].match(/\.css$/i))
      {
        assetLoader.loadStyle(arguments[i]);
      }
      else 
      {
        assetLoader.loadScript(arguments[i]);
      }
    }
    else if(arguments[i].type)
    {
      if(arguments[i].type.match(/text\/css/i))
      {
        assetLoader.loadStyle(arguments[i].url, arguments[i].plugin);
      }
      else if(arguments[i].type.match(/text\/javascript/i))
      {
        assetLoader.loadScript(arguments[i].url, arguments[i].plugin);
      }
    }
    else if(arguments[i].length >= 1)
    {
      if(arguments[i][0].match(/\.css$/i))
      {
        assetLoader.loadStyle(arguments[i][0], arguments[i][1]);
      }
      else 
      {
        assetLoader.loadScript(arguments[i][0], arguments[i][1]);
      }
    }
  }
  
  return assetLoader;
}

/***************************************************
 *  Category: EDITOR UTILITIES
 ***************************************************/
/** Utility function: Outputs the structure of the edited document */
Xinha.prototype.debugTree = function()
{
  var ta = document.createElement("textarea");
  ta.style.width = "100%";
  ta.style.height = "20em";
  ta.value = "";
  function debug(indent, str)
  {
    for ( ; --indent >= 0; )
    {
      ta.value += " ";
    }
    ta.value += str + "\n";
  }
  function _dt(root, level)
  {
    var tag = root.tagName.toLowerCase(), i;
    var ns = Xinha.is_ie ? root.scopeName : root.prefix;
    debug(level, "- " + tag + " [" + ns + "]");
    for ( i = root.firstChild; i; i = i.nextSibling )
    {
      if ( i.nodeType == 1 )
      {
        _dt(i, level + 2);
      }
    }
  }
  _dt(this._doc.body, 0);
  document.body.appendChild(ta);
};
/** Extracts the textual content of a given node
 * @param {DomNode} el
 */

Xinha.getInnerText = function(el)
{
  var txt = '', i;
  for ( i = el.firstChild; i; i = i.nextSibling )
  {
    if ( i.nodeType == 3 )
    {
      txt += i.data;
    }
    else if ( i.nodeType == 1 )
    {
      txt += Xinha.getInnerText(i);
    }
  }
  return txt;
};
/** Cleans dirty HTML from MS word; always cleans the whole editor content
 *  @TODO: move this in a separate file
 *  @TODO: turn this into a static function that cleans a given string
 */
Xinha.prototype._wordClean = function()
{
  var editor = this;
  var stats =
  {
    empty_tags : 0,
    cond_comm  : 0,
    mso_elmts  : 0,
    mso_class  : 0,
    mso_style  : 0,
    mso_xmlel  : 0,
    orig_len   : this._doc.body.innerHTML.length,
    T          : new Date().getTime()
  };
  var stats_txt =
  {
    empty_tags : "Empty tags removed: ",
    cond_comm  : "Conditional comments removed",
    mso_elmts  : "MSO invalid elements removed",
    mso_class  : "MSO class names removed: ",
    mso_style  : "MSO inline style removed: ",
    mso_xmlel  : "MSO XML elements stripped: "
  };

  function showStats()
  {
    var txt = "Xinha word cleaner stats: \n\n";
    for ( var i in stats )
    {
      if ( stats_txt[i] )
      {
        txt += stats_txt[i] + stats[i] + "\n";
      }
    }
    txt += "\nInitial document length: " + stats.orig_len + "\n";
    txt += "Final document length: " + editor._doc.body.innerHTML.length + "\n";
    txt += "Clean-up took " + ((new Date().getTime() - stats.T) / 1000) + " seconds";
    alert(txt);
  }

  function clearClass(node)
  {
    var newc = node.className.replace(/(^|\s)mso.*?(\s|$)/ig, ' ');
    if ( newc != node.className )
    {
      node.className = newc;
      if ( !/\S/.test(node.className))
      {
        node.removeAttribute("className");
        ++stats.mso_class;
      }
    }
  }

  function clearStyle(node)
  {
    var declarations = node.style.cssText.split(/\s*;\s*/);
    for ( var i = declarations.length; --i >= 0; )
    {
      if ( /^mso|^tab-stops/i.test(declarations[i]) || /^margin\s*:\s*0..\s+0..\s+0../i.test(declarations[i]) )
      {
        ++stats.mso_style;
        declarations.splice(i, 1);
      }
    }
    node.style.cssText = declarations.join("; ");
  }

  function removeElements(el)
  {
    if (('link' == el.tagName.toLowerCase() &&
        (el.attributes && /File-List|Edit-Time-Data|themeData|colorSchemeMapping/.test(el.attributes.rel.nodeValue))) ||
        /^(style|meta)$/i.test(el.tagName))
    {
      Xinha.removeFromParent(el);
      ++stats.mso_elmts;
      return true;
    }
    return false;
  }

  function checkEmpty(el)
  {
    // @todo : check if this is quicker
    //  if (!['A','SPAN','B','STRONG','I','EM','FONT'].contains(el.tagName) && !el.firstChild)
    if ( /^(a|span|b|strong|i|em|font|div|p)$/i.test(el.tagName) && !el.firstChild)
    {
      Xinha.removeFromParent(el);
      ++stats.empty_tags;
      return true;
    }
    return false;
  }

  function parseTree(root)
  {
    clearClass(root);
    clearStyle(root);
    var next;
    for (var i = root.firstChild; i; i = next )
    {
      next = i.nextSibling;
      if ( i.nodeType == 1 && parseTree(i) )
      {
        if ((Xinha.is_ie && root.scopeName != 'HTML') || (!Xinha.is_ie && /:/.test(i.tagName)))
        {
          // Nowadays, Word spits out tags like '<o:something />'.  Since the
          // document being cleaned might be HTML4 and not XHTML, this tag is
          // interpreted as '<o:something /="/">'.  For HTML tags without
          // closing elements (e.g. IMG) these two forms are equivalent.  Since
          // HTML does not recognize these tags, however, they end up as
          // parents of elements that should be their siblings.  We reparent
          // the children and remove them from the document.
          for (var index=i.childNodes && i.childNodes.length-1; i.childNodes && i.childNodes.length && i.childNodes[index]; --index)
          {
            if (i.nextSibling)
            {
              i.parentNode.insertBefore(i.childNodes[index],i.nextSibling);
            }
            else
            {
              i.parentNode.appendChild(i.childNodes[index]);
            }
          }
          Xinha.removeFromParent(i);
          continue;
        }
        if (checkEmpty(i))
        {
          continue;
        }
        if (removeElements(i))
        {
          continue;
        }
      }
      else if (i.nodeType == 8)
      {
        // 8 is a comment node, and can contain conditional comments, which
        // will be interpreted by IE as if they were not comments.
        if (/(\s*\[\s*if\s*(([gl]te?|!)\s*)?(IE|mso)\s*(\d+(\.\d+)?\s*)?\]>)/.test(i.nodeValue))
        {
          // We strip all conditional comments directly from the tree.
          Xinha.removeFromParent(i);
          ++stats.cond_comm;
        }
      }
    }
    return true;
  }
  parseTree(this._doc.body);
  // showStats();
  // this.debugTree();
  // this.setHTML(this.getHTML());
  // this.setHTML(this.getInnerHTML());
  // this.forceRedraw();
  this.updateToolbar();
};

/** Removes &lt;font&gt; tags; always cleans the whole editor content
 *  @TODO: move this in a separate file
 *  @TODO: turn this into a static function that cleans a given string
 */
Xinha.prototype._clearFonts = function()
{
  var D = this.getInnerHTML();

  if ( confirm(Xinha._lc("Would you like to clear font typefaces?")) )
  {
    D = D.replace(/face="[^"]*"/gi, '');
    D = D.replace(/font-family:[^;}"']+;?/gi, '');
  }

  if ( confirm(Xinha._lc("Would you like to clear font sizes?")) )
  {
    D = D.replace(/size="[^"]*"/gi, '');
    D = D.replace(/font-size:[^;}"']+;?/gi, '');
  }

  if ( confirm(Xinha._lc("Would you like to clear font colours?")) )
  {
    D = D.replace(/color="[^"]*"/gi, '');
    D = D.replace(/([^\-])color:[^;}"']+;?/gi, '$1');
  }

  D = D.replace(/(style|class)="\s*"/gi, '');
  D = D.replace(/<(font|span)\s*>/gi, '');
  this.setHTML(D);
  this.updateToolbar();
};

Xinha.prototype._splitBlock = function()
{
  this._doc.execCommand('formatblock', false, 'div');
};

/** Sometimes the display has to be refreshed to make DOM changes visible (?) (Gecko bug?)  */
Xinha.prototype.forceRedraw = function()
{
  this._doc.body.style.visibility = "hidden";
  this._doc.body.style.visibility = "";
  // this._doc.body.innerHTML = this.getInnerHTML();
};

/** Focuses the iframe window. 
 * @returns {document} a reference to the editor document
 */
Xinha.prototype.focusEditor = function()
{
  switch (this._editMode)
  {
    // notice the try { ... } catch block to avoid some rare exceptions in FireFox
    // (perhaps also in other Gecko browsers). Manual focus by user is required in
    // case of an error. Somebody has an idea?
    case "wysiwyg" :
      try
      {
        // We don't want to focus the field unless at least one field has been activated.
        if ( Xinha._someEditorHasBeenActivated )
        {
          this.activateEditor(); // Ensure *this* editor is activated
          this._iframe.contentWindow.focus(); // and focus it
        }
      } catch (ex) {}
    break;
    case "textmode":
      try
      {
        this._textArea.focus();
      } catch (e) {}
    break;
    default:
      alert("ERROR: mode " + this._editMode + " is not defined");
  }
  return this._doc;
};

/** Takes a snapshot of the current text (for undo)
 * @private
 */
Xinha.prototype._undoTakeSnapshot = function()
{
  ++this._undoPos;
  if ( this._undoPos >= this.config.undoSteps )
  {
    // remove the first element
    this._undoQueue.shift();
    --this._undoPos;
  }
  // use the fasted method (getInnerHTML);
  var take = true;
  var txt = this.getInnerHTML();
  if ( this._undoPos > 0 )
  {
    take = (this._undoQueue[this._undoPos - 1] != txt);
  }
  if ( take )
  {
    this._undoQueue[this._undoPos] = txt;
  }
  else
  {
    this._undoPos--;
  }
};
/** Custom implementation of undo functionality
 * @private
 */
Xinha.prototype.undo = function()
{
  if ( this._undoPos > 0 )
  {
    var txt = this._undoQueue[--this._undoPos];
    if ( txt )
    {
      this.setHTML(txt);
    }
    else
    {
      ++this._undoPos;
    }
  }
};
/** Custom implementation of redo functionality
 * @private
 */
Xinha.prototype.redo = function()
{
  if ( this._undoPos < this._undoQueue.length - 1 )
  {
    var txt = this._undoQueue[++this._undoPos];
    if ( txt )
    {
      this.setHTML(txt);
    }
    else
    {
      --this._undoPos;
    }
  }
};
/** Disables (greys out) the buttons of the toolbar
 * @param {Array} except this array contains ids of toolbar objects that will not be disabled
 */
Xinha.prototype.disableToolbar = function(except)
{
  if ( this._timerToolbar )
  {
    clearTimeout(this._timerToolbar);
  }
  if ( typeof except == 'undefined' )
  {
    except = [ ];
  }
  else if ( typeof except != 'object' )
  {
    except = [except];
  }

  for ( var i in this._toolbarObjects )
  {
    var btn = this._toolbarObjects[i];
    if ( except.contains(i) )
    {
      continue;
    }
    // prevent iterating over wrong type
    if ( typeof btn.state != 'function' )
    {
      continue;
    }
    btn.state("enabled", false);
  }
};
/** Enables the toolbar again when disabled by disableToolbar() */
Xinha.prototype.enableToolbar = function()
{
  this.updateToolbar();
};

/** Updates enabled/disable/active state of the toolbar elements, the statusbar and other things
 *  This function is called on every key stroke as well as by a timer on a regular basis.<br />
 *  Plugins have the opportunity to implement a prototype.onUpdateToolbar() method, which will also
 *  be called by this function.
 * @param {Boolean} noStatus private use Exempt updating of statusbar
 */
// FIXME : this function needs to be splitted in more functions.
// It is actually to heavy to be understable and very scary to manipulate
Xinha.prototype.updateToolbar = function(noStatus)
{
  if (this.suspendUpdateToolbar)
  {
    return;
  }
  var doc = this._doc;
  var text = (this._editMode == "textmode");
  var ancestors = null;
  if ( !text )
  {
    ancestors = this.getAllAncestors();
    if ( this.config.statusBar && !noStatus )
    {
      while ( this._statusBarItems.length )
      { 
        var item = this._statusBarItems.pop();
        item.el = null;
        item.editor = null;
        item.onclick = null;
        item.oncontextmenu = null;
        item._xinha_dom0Events.click = null;
        item._xinha_dom0Events.contextmenu = null;
        item = null;
      }

      this._statusBarTree.innerHTML = ' ';
      this._statusBarTree.appendChild(document.createTextNode(Xinha._lc("Path") + ": ")); 
      for ( var i = ancestors.length; --i >= 0; )
      {
        var el = ancestors[i];
        if ( !el )
        {
          // hell knows why we get here; this
          // could be a classic example of why
          // it's good to check for conditions
          // that are impossible to happen ;-)
          continue;
        }
        var a = document.createElement("a");
        a.href = "javascript:void(0);";
        a.el = el;
        a.editor = this;
        this._statusBarItems.push(a);
        Xinha.addDom0Event(
          a,
          'click',
          function() {
            this.blur();
            this.editor.selectNodeContents(this.el);
            this.editor.updateToolbar(true);
            return false;
          }
        );
        Xinha.addDom0Event(
          a,
          'contextmenu',
          function()
          {
            // TODO: add context menu here
            this.blur();
            var info = "Inline style:\n\n";
            info += this.el.style.cssText.split(/;\s*/).join(";\n");
            alert(info);
            return false;
          }
        );
        var txt = el.tagName.toLowerCase();
        switch (txt)
        {
          case 'b':
            txt = 'strong';
          break;
          case 'i':
            txt = 'em';
          break;
          case 'strike':
            txt = 'del';
          break;
        }
        if (typeof el.style != 'undefined')
        {
          a.title = el.style.cssText;
        }
        if ( el.id )
        {
          txt += "#" + el.id;
        }
        if ( el.className )
        {
          txt += "." + el.className;
        }
        a.appendChild(document.createTextNode(txt));
        this._statusBarTree.appendChild(a);
        if ( i !== 0 )
        {
          this._statusBarTree.appendChild(document.createTextNode(String.fromCharCode(0xbb)));
        }
        Xinha.freeLater(a);
      }
    }
  }

  for ( var cmd in this._toolbarObjects )
  {
    var btn = this._toolbarObjects[cmd];
    var inContext = true;
    // prevent iterating over wrong type
    if ( typeof btn.state != 'function' )
    {
      continue;
    }
    if ( btn.context && !text )
    {
      inContext = false;
      var context = btn.context;
      var attrs = [];
      if ( /(.*)\[(.*?)\]/.test(context) )
      {
        context = RegExp.$1;
        attrs = RegExp.$2.split(",");
      }
      context = context.toLowerCase();
      var match = (context == "*");
      for ( var k = 0; k < ancestors.length; ++k )
      {
        if ( !ancestors[k] )
        {
          // the impossible really happens.
          continue;
        }
        if ( match || ( ancestors[k].tagName.toLowerCase() == context ) )
        {
          inContext = true;
          var contextSplit = null;
          var att = null;
          var comp = null;
          var attVal = null;
          for ( var ka = 0; ka < attrs.length; ++ka )
          {
            contextSplit = attrs[ka].match(/(.*)(==|!=|===|!==|>|>=|<|<=)(.*)/);
            att = contextSplit[1];
            comp = contextSplit[2];
            attVal = contextSplit[3];

            if (!eval(ancestors[k][att] + comp + attVal))
            {
              inContext = false;
              break;
            }
          }
          if ( inContext )
          {
            break;
          }
        }
      }
    }
    btn.state("enabled", (!text || btn.text) && inContext);
    if ( typeof cmd == "function" )
    {
      continue;
    }
    // look-it-up in the custom dropdown boxes
    var dropdown = this.config.customSelects[cmd];
    if ( ( !text || btn.text ) && ( typeof dropdown != "undefined" ) )
    {
      dropdown.refresh(this);
      continue;
    }
    switch (cmd)
    {
      case "fontname":
      case "fontsize":
        if ( !text )
        {
          try
          {
            var value = ("" + doc.queryCommandValue(cmd)).toLowerCase();
            if ( !value )
            {
              btn.element.selectedIndex = 0;
              break;
            }

            // HACK -- retrieve the config option for this
            // combo box.  We rely on the fact that the
            // variable in config has the same name as
            // button name in the toolbar.
            var options = this.config[cmd];
            var sIndex = 0;
            for ( var j in options )
            {
            // FIXME: the following line is scary.
              if ( ( j.toLowerCase() == value ) || ( options[j].substr(0, value.length).toLowerCase() == value ) )
              {
                btn.element.selectedIndex = sIndex;
                throw "ok";
              }
              ++sIndex;
            }
            btn.element.selectedIndex = 0;
          } catch(ex) {}
        }
      break;

      // It's better to search for the format block by tag name from the
      //  current selection upwards, because IE has a tendancy to return
      //  things like 'heading 1' for 'h1', which breaks things if you want
      //  to call your heading blocks 'header 1'.  Stupid MS.
      case "formatblock":
        var blocks = [];
        for ( var indexBlock in this.config.formatblock )
        {
	  var blockname = this.config.formatblock[indexBlock];
          // prevent iterating over wrong type
          if ( typeof blockname  == 'string' )
          {
            blocks[blocks.length] = this.config.formatblockDetector[blockname] || blockname;
          }
        }

        var match = this._getFirstAncestorAndWhy(this.getSelection(), blocks);
        var deepestAncestor = match[0];
        var matchIndex = match[1];


        if ( deepestAncestor )
        {
          // the function can return null for its second element even if a match is found,
          // but we passed in an array, so we know it will be a numerical index.
	  btn.element.selectedIndex = matchIndex;
        }
        else
        {
          btn.element.selectedIndex = 0;
        }
      break;

      case "textindicator":
        if ( !text )
        {
          try
          {
            var style = btn.element.style;
            style.backgroundColor = Xinha._makeColor(doc.queryCommandValue(Xinha.is_ie ? "backcolor" : "hilitecolor"));
            if ( /transparent/i.test(style.backgroundColor) )
            {
              // Mozilla
              style.backgroundColor = Xinha._makeColor(doc.queryCommandValue("backcolor"));
            }
            style.color = Xinha._makeColor(doc.queryCommandValue("forecolor"));
            style.fontFamily = doc.queryCommandValue("fontname");
            style.fontWeight = doc.queryCommandState("bold") ? "bold" : "normal";
            style.fontStyle = doc.queryCommandState("italic") ? "italic" : "normal";
          } catch (ex) {
            // alert(e + "\n\n" + cmd);
          }
        }
      break;

      case "htmlmode":
        btn.state("active", text);
      break;

      case "lefttoright":
      case "righttoleft":
        var eltBlock = this.getParentElement();
        while ( eltBlock && !Xinha.isBlockElement(eltBlock) )
        {
          eltBlock = eltBlock.parentNode;
        }
        if ( eltBlock )
        {
          btn.state("active", (eltBlock.style.direction == ((cmd == "righttoleft") ? "rtl" : "ltr")));
        }
      break;

      default:
        cmd = cmd.replace(/(un)?orderedlist/i, "insert$1orderedlist");
        try
        {
          btn.state("active", (!text && doc.queryCommandState(cmd)));
        } catch (ex) {}
      break;
    }
  }
  // take undo snapshots
  if ( this._customUndo && !this._timerUndo )
  {
    this._undoTakeSnapshot();
    var editor = this;
    this._timerUndo = setTimeout(function() { editor._timerUndo = null; }, this.config.undoTimeout);
  }
  this.firePluginEvent('onUpdateToolbar');
};

/** Returns a editor object referenced by the id or name of the textarea or the textarea node itself
 * For example to retrieve the HTML of an editor made out of the textarea with the id "myTextArea" you would do<br />
 * <code>
 *	 var editor = Xinha.getEditor("myTextArea");
 *   var html = editor.getEditorContent(); 
 * </code>
 * @returns {Xinha|null} 
 * @param {String|DomNode} ref id or name of the textarea or the textarea node itself
 */
Xinha.getEditor = function(ref)
{
  for ( var i = __xinhas.length; i--; )
  {
    var editor = __xinhas[i];
    if ( editor && ( editor._textArea.id == ref || editor._textArea.name == ref || editor._textArea == ref ) )
    {
      return editor;
    }
  }
  return null;
};
/** Sometimes one wants to call a plugin method directly, e.g. from outside the editor.
 * This function returns the respective editor's instance of a plugin.
 * For example you might want to have a button to trigger SaveSubmit's save() method:<br />
 * <code>
 *	 &lt;button type="button" onclick="Xinha.getEditor('myTextArea').getPluginInstance('SaveSubmit').save();return false;"&gt;Save&lt;/button&gt;
 * </code>
 * @returns {PluginObject|null} 
 * @param {String} plugin name of the plugin
 */
Xinha.prototype.getPluginInstance = function (plugin)
{
  if (this.plugins[plugin])
  {
    return this.plugins[plugin].instance;
  }
  else
  {
    return null;
  }
};
/** Returns an array with all the ancestor nodes of the selection or current cursor position.
* @returns {Array}
*/
Xinha.prototype.getAllAncestors = function()
{
  var p = this.getParentElement();
  var a = [];
  while ( p && (p.nodeType == 1) && ( p.tagName.toLowerCase() != 'body' ) )
  {
    a.push(p);
    p = p.parentNode;
  }
  a.push(this._doc.body);
  return a;
};

/** Traverses the DOM upwards and returns the first element that is of one of the specified types
 *  @param {Selection} sel  Selection object as returned by getSelection
 *  @param {Array|String} types Array of matching criteria.  Each criteria is either a string containing the tag name, or a callback used to select the element.
 *  @returns {DomNode|null} 
 */
Xinha.prototype._getFirstAncestor = function(sel, types)
{
  return this._getFirstAncestorAndWhy(sel, types)[0];
};

/** Traverses the DOM upwards and returns the first element that is one of the specified types,
 *  and which (of the specified types) the found element successfully matched.
 *  @param {Selection} sel  Selection object as returned by getSelection
 *  @param {Array|String} types Array of matching criteria.  Each criteria is either a string containing the tag name, or a callback used to select the element.
 *  @returns {Array} The array will look like [{DomNode|null}, {Integer|null}] -- that is, it always contains two elements.  The first element is the element that matched, or null if no match was found. The second is the numerical index that can be used to identify which element of the "types" was responsible for the match.  It will be null if no match was found.  It will also be null if the "types" argument was omitted. 
 */
Xinha.prototype._getFirstAncestorAndWhy = function(sel, types)
{
  var prnt = this.activeElement(sel);
  if ( prnt === null )
  {
    // Hmm, I think Xinha.getParentElement() would do the job better?? - James
    try
    {
      prnt = (Xinha.is_ie ? this.createRange(sel).parentElement() : this.createRange(sel).commonAncestorContainer);
    }
    catch(ex)
    {
      return [null, null];
    }
  }

  if ( typeof types == 'string' )
  {
    types = [types];
  }

  while ( prnt )
  {
    if ( prnt.nodeType == 1 )
    {
      if ( types === null )
      {
	return [prnt, null];
      }
      for (var index=0; index<types.length; ++index) {
        if (typeof types[index] == 'string' && types[index] == prnt.tagName.toLowerCase()){
          // Criteria is a tag name.  It matches
	  return [prnt, index];
      }
        else if (typeof types[index] == 'function' && types[index](this, prnt)) {
          // Criteria is a callback.  It matches
	  return [prnt, index];
        }
      }

      if ( prnt.tagName.toLowerCase() == 'body' )
      {
        break;
      }
      if ( prnt.tagName.toLowerCase() == 'table' )
      {
        break;
      }
    }
    prnt = prnt.parentNode;
  }

  return [null, null];
};

/** Traverses the DOM upwards and returns the first element that is a block level element
 *  @param {Selection} sel  Selection object as returned by getSelection
 *  @returns {DomNode|null} 
 */
Xinha.prototype._getAncestorBlock = function(sel)
{
  // Scan upwards to find a block level element that we can change or apply to
  var prnt = (Xinha.is_ie ? this.createRange(sel).parentElement : this.createRange(sel).commonAncestorContainer);

  while ( prnt && ( prnt.nodeType == 1 ) )
  {
    switch ( prnt.tagName.toLowerCase() )
    {
      case 'div':
      case 'p':
      case 'address':
      case 'blockquote':
      case 'center':
      case 'del':
      case 'ins':
      case 'pre':
      case 'h1':
      case 'h2':
      case 'h3':
      case 'h4':
      case 'h5':
      case 'h6':
      case 'h7':
        // Block Element
        return prnt;

      case 'body':
      case 'noframes':
      case 'dd':
      case 'li':
      case 'th':
      case 'td':
      case 'noscript' :
        // Halting element (stop searching)
        return null;

      default:
        // Keep lookin
        break;
    }
  }

  return null;
};

/** What's this? does nothing, has to be removed
 * 
 * @deprecated
 */
Xinha.prototype._createImplicitBlock = function(type)
{
  // expand it until we reach a block element in either direction
  // then wrap the selection in a block and return
  var sel = this.getSelection();
  if ( Xinha.is_ie )
  {
    sel.empty();
  }
  else
  {
    sel.collapseToStart();
  }

  var rng = this.createRange(sel);

  // Expand UP

  // Expand DN
};



/**
 *  Call this function to surround the existing HTML code in the selection with
 *  your tags.  FIXME: buggy! Don't use this 
 * @todo: when will it be deprecated ? Can it be removed already ?
 * @private (tagged private to not further promote use of this function)
 * @deprecated
 */
Xinha.prototype.surroundHTML = function(startTag, endTag)
{
  var html = this.getSelectedHTML();
  // the following also deletes the selection
  this.insertHTML(startTag + html + endTag);
};

/** Return true if we have some selection
 *  @returns {Boolean} 
 */
Xinha.prototype.hasSelectedText = function()
{
  // FIXME: come _on_ mishoo, you can do better than this ;-)
  return this.getSelectedHTML() !== '';
};

/***************************************************
 *  Category: EVENT HANDLERS
 ***************************************************/

/** onChange handler for dropdowns in toolbar 
 *  @private
 *  @param {DomNode} el Reference to the SELECT object
 *  @param {String} txt  The name of the select field, as in config.toolbar
 *  @returns {DomNode|null} 
 */
Xinha.prototype._comboSelected = function(el, txt)
{
  this.focusEditor();
  var value = el.options[el.selectedIndex].value;
  switch (txt)
  {
    case "fontname":
    case "fontsize":
      this.execCommand(txt, false, value);
    break;
    case "formatblock":
      // Mozilla inserts an empty tag (<>) if no parameter is passed  
      if ( !value )
      {
      	this.updateToolbar();
      	break;
      }
      if( !Xinha.is_gecko || value !== 'blockquote' )
      {
        value = "<" + value + ">";
      }
      this.execCommand(txt, false, value);
    break;
    default:
      // try to look it up in the registered dropdowns
      var dropdown = this.config.customSelects[txt];
      if ( typeof dropdown != "undefined" )
      {
        dropdown.action(this, value, el, txt);
      }
      else
      {
        alert("FIXME: combo box " + txt + " not implemented");
      }
    break;
  }
};

/** Open a popup to select the hilitecolor or forecolor
 * @private
 * @param {String} cmdID The commande ID (hilitecolor or forecolor)
 */
Xinha.prototype._colorSelector = function(cmdID)
{
  var editor = this;	// for nested functions

  // backcolor only works with useCSS/styleWithCSS (see mozilla bug #279330 & Midas doc)
  // and its also nicer as <font>
  if ( Xinha.is_gecko )
  {
    try
    {
     editor._doc.execCommand('useCSS', false, false); // useCSS deprecated & replaced by styleWithCSS 
     editor._doc.execCommand('styleWithCSS', false, true); 

    } catch (ex) {}
  }
  
  var btn = editor._toolbarObjects[cmdID].element;
  var initcolor;
  if ( cmdID == 'hilitecolor' )
  {
    if ( Xinha.is_ie )
    {
      cmdID = 'backcolor';
      initcolor = Xinha._colorToRgb(editor._doc.queryCommandValue("backcolor"));
    }
    else
    {
      initcolor = Xinha._colorToRgb(editor._doc.queryCommandValue("hilitecolor"));
    }
  }
  else
  {
  	initcolor = Xinha._colorToRgb(editor._doc.queryCommandValue("forecolor"));
  }
  var cback = function(color) { editor._doc.execCommand(cmdID, false, color); };
  if ( Xinha.is_ie )
  {
    var range = editor.createRange(editor.getSelection());
    cback = function(color)
    {
      range.select();
      editor._doc.execCommand(cmdID, false, color);
    };
  }
  var picker = new Xinha.colorPicker(
  {
  	cellsize:editor.config.colorPickerCellSize,
  	callback:cback,
  	granularity:editor.config.colorPickerGranularity,
  	websafe:editor.config.colorPickerWebSafe,
  	savecolors:editor.config.colorPickerSaveColors
  });
  picker.open(editor.config.colorPickerPosition, btn, initcolor);
};

/** This is a wrapper for the browser's execCommand function that handles things like 
 *  formatting, inserting elements, etc.<br />
 *  It intercepts some commands and replaces them with our own implementation.<br />
 *  It provides a hook for the "firePluginEvent" system ("onExecCommand").<br /><br />
 *  For reference see:<br />
 *     <a href="http://www.mozilla.org/editor/midas-spec.html">Mozilla implementation</a><br />
 *     <a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/execcommand.asp">MS implementation</a>
 *
 *  @see Xinha#firePluginEvent
 *  @param {String} cmdID command to be executed as defined in the browsers implemantations or Xinha custom
 *  @param {Boolean} UI for compatibility with the execCommand syntax; false in most (all) cases
 *  @param {Mixed} param Some commands require parameters
 *  @returns {Boolean} always false 
 */
Xinha.prototype.execCommand = function(cmdID, UI, param)
{
  var editor = this;	// for nested functions
  this.focusEditor();
  cmdID = cmdID.toLowerCase();
  
  // See if any plugins want to do something special
  if(this.firePluginEvent('onExecCommand', cmdID, UI, param))
  {
    this.updateToolbar();
    return false;
  }

  switch (cmdID)
  {
    case "htmlmode":
      this.setMode();
    break;

    case "hilitecolor":
    case "forecolor":
      this._colorSelector(cmdID);
    break;

    case "createlink":
      this._createLink();
    break;

    case "undo":
    case "redo":
      if (this._customUndo)
      {
        this[cmdID]();
      }
      else
      {
        this._doc.execCommand(cmdID, UI, param);
      }
    break;

    case "inserttable":
      this._insertTable();
    break;

    case "insertimage":
      this._insertImage();
    break;

    case "showhelp":
      this._popupDialog(editor.config.URIs.help, null, this);
    break;

    case "killword":
      this._wordClean();
    break;

    case "cut":
    case "copy":
    case "paste":
      this._doc.execCommand(cmdID, UI, param);
      if ( this.config.killWordOnPaste )
      {
        this._wordClean();
      }
    break;
    case "lefttoright":
    case "righttoleft":
      if (this.config.changeJustifyWithDirection) 
      {
        this._doc.execCommand((cmdID == "righttoleft") ? "justifyright" : "justifyleft", UI, param);
      }
      var dir = (cmdID == "righttoleft") ? "rtl" : "ltr";
      var el = this.getParentElement();
      while ( el && !Xinha.isBlockElement(el) )
      {
        el = el.parentNode;
      }
      if ( el )
      {
        if ( el.style.direction == dir )
        {
          el.style.direction = "";
        }
        else
        {
          el.style.direction = dir;
        }
      }
    break;
    
    case 'justifyleft'  :
    case 'justifyright' :
      cmdID.match(/^justify(.*)$/);
      var ae = this.activeElement(this.getSelection());      
      if(ae && ae.tagName.toLowerCase() == 'img')
      {
        ae.align = ae.align == RegExp.$1 ? '' : RegExp.$1;
      }
      else
      {
        this._doc.execCommand(cmdID, UI, param);
      }
    break;
    
    default:
      try
      {
        this._doc.execCommand(cmdID, UI, param);
      }
      catch(ex)
      {
        if ( this.config.debug )
        {
          alert(ex + "\n\nby execCommand(" + cmdID + ");");
        }
      }
    break;
  }

  this.updateToolbar();
  return false;
};

/** A generic event handler for things that happen in the IFRAME's document.<br />
 *  It provides two hooks for the "firePluginEvent" system:<br />
 *   "onKeyPress"<br />
 *   "onMouseDown"
 *  @see Xinha#firePluginEvent
 *  @param {Event} ev
 */
Xinha.prototype._editorEvent = function(ev)
{
  var editor = this;

  //call events of textarea
  if ( typeof editor._textArea['on'+ev.type] == "function" )
  {
    editor._textArea['on'+ev.type](ev);
  }
  
  if ( this.isKeyEvent(ev) )
  {
    // Run the ordinary plugins first
    if(editor.firePluginEvent('onKeyPress', ev))
    {
      return false;
    }
    
    // Handle the core shortcuts
    if ( this.isShortCut( ev ) )
    {
      this._shortCuts(ev);
    }
  }

  if ( ev.type == 'mousedown' )
  {
    if(editor.firePluginEvent('onMouseDown', ev))
    {
      return false;
    }
  }

  // update the toolbar state after some time
  if ( editor._timerToolbar )
  {
    clearTimeout(editor._timerToolbar);
  }
  if (!this.suspendUpdateToolbar)
  {
  editor._timerToolbar = setTimeout(
    function()
    {
      editor.updateToolbar();
      editor._timerToolbar = null;
    },
    250);
  }
};

/** Handle double click events.
 *  See dblclickList in the config.
 */
 
Xinha.prototype._onDoubleClick = function(ev)
{
  var editor=this;
  var target = Xinha.is_ie ? ev.srcElement : ev.target;
  var tag = target.tagName;
  var className = target.className;
  if (tag) {
    tag = tag.toLowerCase();
    if (className && (this.config.dblclickList[tag+"."+className] != undefined))
      this.config.dblclickList[tag+"."+className][0](editor, target);
    else if (this.config.dblclickList[tag] != undefined)
      this.config.dblclickList[tag][0](editor, target);
  };
};

/** Handles ctrl + key shortcuts 
 *  @TODO: make this mor flexible
 *  @private
 *  @param {Event} ev
 */
Xinha.prototype._shortCuts = function (ev)
{
  var key = this.getKey(ev).toLowerCase();
  var cmd = null;
  var value = null;
  switch (key)
  {
    // simple key commands follow

    case 'b': cmd = "bold"; break;
    case 'i': cmd = "italic"; break;
    case 'u': cmd = "underline"; break;
    case 's': cmd = "strikethrough"; break;
    case 'l': cmd = "justifyleft"; break;
    case 'e': cmd = "justifycenter"; break;
    case 'r': cmd = "justifyright"; break;
    case 'j': cmd = "justifyfull"; break;
    case 'z': cmd = "undo"; break;
    case 'y': cmd = "redo"; break;
    case 'v': cmd = "paste"; break;
    case 'n':
    cmd = "formatblock";
    value = "p";
    break;

    case '0': cmd = "killword"; break;

    // headings
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    cmd = "formatblock";
    value = "h" + key;
    break;
  }
  if ( cmd )
  {
    // execute simple command
    this.execCommand(cmd, false, value);
    Xinha._stopEvent(ev);
  }
};
/** Changes the type of a given node
 *  @param {DomNode} el The element to convert
 *  @param {String} newTagName The type the element will be converted to
 *  @returns {DomNode} A reference to the new element
 */
Xinha.prototype.convertNode = function(el, newTagName)
{
  var newel = this._doc.createElement(newTagName);
  while ( el.firstChild )
  {
    newel.appendChild(el.firstChild);
  }
  return newel;
};

/** Scrolls the editor iframe to a given element or to the cursor
 *  @param {DomNode} e optional The element to scroll to; if ommitted, element the element the cursor is in
 */
Xinha.prototype.scrollToElement = function(e)
{
  if(!e)
  {
    e = this.getParentElement();
    if(!e)
    {
      return;
    }
  }
  
  // This was at one time limited to Gecko only, but I see no reason for it to be. - James
  var position = Xinha.getElementTopLeft(e);  
  this._iframe.contentWindow.scrollTo(position.left, position.top);
};

/** Get the edited HTML
 *  
 *  @public
 *  @returns {String} HTML content
 */
Xinha.prototype.getEditorContent = function()
{
  return this.outwardHtml(this.getHTML());
};

/** Completely change the HTML inside the editor
 *
 *  @public
 *  @param {String} html new content
 */
Xinha.prototype.setEditorContent = function(html)
{
  this.setHTML(this.inwardHtml(html));
};
/** Saves the contents of all Xinhas to their respective textareas
 *  @public 
 */
Xinha.updateTextareas = function()
{
  var e;
  for (var i=0;i<__xinhas.length;i++)
  {
    e = __xinhas[i];
    e._textArea.value = e.getEditorContent();
  }
}
/** Get the raw edited HTML, should not be used without Xinha.prototype.outwardHtml()
 *  
 *  @private
 *  @returns {String} HTML content
 */
Xinha.prototype.getHTML = function()
{
  var html = '';
  switch ( this._editMode )
  {
    case "wysiwyg":
      if ( !this.config.fullPage )
      {
        html = Xinha.getHTML(this._doc.body, false, this).trim();
      }
      else
      {
        html = this.doctype + "\n" + Xinha.getHTML(this._doc.documentElement, true, this);
      }
    break;
    case "textmode":
      html = this._textArea.value;
    break;
    default:
      alert("Mode <" + this._editMode + "> not defined!");
      return false;
  }
  return html;
};

/** Performs various transformations of the HTML used internally, complement to Xinha.prototype.inwardHtml()  
 *  Plugins can provide their own, additional transformations by defining a plugin.prototype.outwardHtml() implematation,
 *  which is called by this function
 *
 *  @private
 *  @see Xinha#inwardHtml
 *  @param {String} html
 *  @returns {String} HTML content
 */
Xinha.prototype.outwardHtml = function(html)
{
  for ( var i in this.plugins )
  {
    var plugin = this.plugins[i].instance;    
    if ( plugin && typeof plugin.outwardHtml == "function" )
    {
      html = plugin.outwardHtml(html);
    }
  }
  
  html = html.replace(/<(\/?)b(\s|>|\/)/ig, "<$1strong$2");
  html = html.replace(/<(\/?)i(\s|>|\/)/ig, "<$1em$2");
  html = html.replace(/<(\/?)strike(\s|>|\/)/ig, "<$1del$2");
  
  // remove disabling of inline event handle inside Xinha iframe
  html = html.replace(/(<[^>]*on(click|mouse(over|out|up|down))=['"])if\(window\.parent &amp;&amp; window\.parent\.Xinha\)\{return false\}/gi,'$1');

  // Figure out what our server name is, and how it's referenced
  var serverBase = location.href.replace(/(https?:\/\/[^\/]*)\/.*/, '$1') + '/';

  // IE puts this in can't figure out why
  //  leaving this in the core instead of InternetExplorer 
  //  because it might be something we are doing so could present itself
  //  in other browsers - James 
  html = html.replace(/https?:\/\/null\//g, serverBase);

  // Make semi-absolute links to be truely absolute
  //  we do this just to standardize so that special replacements knows what
  //  to expect
  html = html.replace(/((href|src|background)=[\'\"])\/+/ig, '$1' + serverBase);

  html = this.outwardSpecialReplacements(html);

  html = this.fixRelativeLinks(html);

  if ( this.config.sevenBitClean )
  {
    html = html.replace(/[^ -~\r\n\t]/g, function(c) { return (c != Xinha.cc) ? '&#'+c.charCodeAt(0)+';' : c; });
  }

  //prevent execution of JavaScript (Ticket #685)
  html = html.replace(/(<script[^>]*((type=[\"\']text\/)|(language=[\"\'])))(freezescript)/gi,"$1javascript");

  // If in fullPage mode, strip the coreCSS
  if(this.config.fullPage)
  {
    html = Xinha.stripCoreCSS(html);
  }

  if (typeof this.config.outwardHtml == 'function' )
  {
    html = this.config.outwardHtml(html);
  }

  return html;
};

/** Performs various transformations of the HTML to be edited 
 *  Plugins can provide their own, additional transformations by defining a plugin.prototype.inwardHtml() implematation,
 *  which is called by this function
 *  
 *  @private
 *  @see Xinha#outwardHtml
 *  @param {String} html  
 *  @returns {String} transformed HTML
 */
Xinha.prototype.inwardHtml = function(html)
{  
  for ( var i in this.plugins )
  {
    var plugin = this.plugins[i].instance;    
    if ( plugin && typeof plugin.inwardHtml == "function" )
    {
      html = plugin.inwardHtml(html);
    }    
  }
    
  // Both IE and Gecko use strike instead of del (#523)
  html = html.replace(/<(\/?)del(\s|>|\/)/ig, "<$1strike$2");

  // disable inline event handle inside Xinha iframe
  html = html.replace(/(<[^>]*on(click|mouse(over|out|up|down))=["'])/gi,'$1if(window.parent &amp;&amp; window.parent.Xinha){return false}');
  
  html = this.inwardSpecialReplacements(html);

  html = html.replace(/(<script)(?![^>]*((type=[\"\']text\/)|(language=[\"\']))javascript[\"\'])/gi,'$1 type="text/javascript"');
  html = html.replace(/(<script[^>]*((type=[\"\']text\/)|(language=[\"\'])))(javascript)/gi,"$1freezescript");

  // For IE's sake, make any URLs that are semi-absolute (="/....") to be
  // truely absolute
  var nullRE = new RegExp('((href|src|background)=[\'"])/+', 'gi');
  html = html.replace(nullRE, '$1' + location.href.replace(/(https?:\/\/[^\/]*)\/.*/, '$1') + '/');

  html = this.fixRelativeLinks(html);
  
  // If in fullPage mode, add the coreCSS
  if(this.config.fullPage)
  {
    html = Xinha.addCoreCSS(html);
  }

  if (typeof this.config.inwardHtml == 'function' )
  {
    html = this.config.inwardHtml(html);
  }

  return html;
};
/** Apply the replacements defined in Xinha.Config.specialReplacements
 *  
 *  @private
 *  @see Xinha#inwardSpecialReplacements
 *  @param {String} html
 *  @returns {String}  transformed HTML
 */
Xinha.prototype.outwardSpecialReplacements = function(html)
{
  for ( var i in this.config.specialReplacements )
  {
    var from = this.config.specialReplacements[i];
    var to   = i; // why are declaring a new variable here ? Seems to be better to just do : for (var to in config)
    // prevent iterating over wrong type
    if ( typeof from.replace != 'function' || typeof to.replace != 'function' )
    {
      continue;
    } 
    // alert('out : ' + from + '=>' + to);
    var reg = new RegExp(Xinha.escapeStringForRegExp(from), 'g');
    html = html.replace(reg, to.replace(/\$/g, '$$$$'));
    //html = html.replace(from, to);
  }
  return html;
};
/** Apply the replacements defined in Xinha.Config.specialReplacements
 *  
 *  @private
 *  @see Xinha#outwardSpecialReplacements
 *  @param {String} html
 *  @returns {String}  transformed HTML
 */
Xinha.prototype.inwardSpecialReplacements = function(html)
{
  // alert("inward");
  for ( var i in this.config.specialReplacements )
  {
    var from = i; // why are declaring a new variable here ? Seems to be better to just do : for (var from in config)
    var to   = this.config.specialReplacements[i];
    // prevent iterating over wrong type
    if ( typeof from.replace != 'function' || typeof to.replace != 'function' )
    {
      continue;
    }
    // alert('in : ' + from + '=>' + to);
    //
    // html = html.replace(reg, to);
    // html = html.replace(from, to);
    var reg = new RegExp(Xinha.escapeStringForRegExp(from), 'g');
    html = html.replace(reg, to.replace(/\$/g, '$$$$')); // IE uses doubled dollar signs to escape backrefs, also beware that IE also implements $& $_ and $' like perl.
  }
  return html;
};
/** Transforms the paths in src & href attributes
 *  
 *  @private
 *  @see Xinha.Config#expandRelativeUrl
 *  @see Xinha.Config#stripSelfNamedAnchors
 *  @see Xinha.Config#stripBaseHref
 *  @see Xinha.Config#baseHref
 *  @param {String} html 
 *  @returns {String} transformed HTML
 */
Xinha.prototype.fixRelativeLinks = function(html)
{
  if ( typeof this.config.expandRelativeUrl != 'undefined' && this.config.expandRelativeUrl ) 
  {
    if (html == null)
    {
      return "";
    }
    var src = html.match(/(src|href)="([^"]*)"/gi);
    var b = document.location.href;
    if ( src )
    {
      var url,url_m,relPath,base_m,absPath;
      for ( var i=0;i<src.length;++i )
      {
        url = src[i].match(/(src|href)="([^"]*)"/i);
        url_m = url[2].match( /\.\.\//g );
        if ( url_m )
        {
          relPath = new RegExp( "(.*?)(([^\/]*\/){"+ url_m.length+"})[^\/]*$" );
          base_m = b.match( relPath );
          absPath = url[2].replace(/(\.\.\/)*/,base_m[1]);
          html = html.replace( new RegExp(Xinha.escapeStringForRegExp(url[2])),absPath );
        }
      }
    }
  }
  
  if ( typeof this.config.stripSelfNamedAnchors != 'undefined' && this.config.stripSelfNamedAnchors )
  {
    var stripRe = new RegExp("((href|src|background)=\")("+Xinha.escapeStringForRegExp(window.unescape(document.location.href.replace(/&/g,'&amp;'))) + ')([#?][^\'" ]*)', 'g');
    html = html.replace(stripRe, '$1$4');
  }

  if ( typeof this.config.stripBaseHref != 'undefined' && this.config.stripBaseHref )
  {
    var baseRe = null;
    if ( typeof this.config.baseHref != 'undefined' && this.config.baseHref !== null )
    {
      baseRe = new RegExp( "((href|src|background|action)=\")(" + Xinha.escapeStringForRegExp(this.config.baseHref.replace(/([^\/]\/)(?=.+\.)[^\/]*$/, "$1")) + ")", 'g' );
	  html = html.replace(baseRe, '$1');
    }
    baseRe = new RegExp( "((href|src|background|action)=\")(" +  Xinha.escapeStringForRegExp(document.location.href.replace( /^(https?:\/\/[^\/]*)(.*)/, '$1' )) + ")", 'g' );
    html = html.replace(baseRe, '$1');
  }

  return html;
};

/** retrieve the HTML (fastest version, but uses innerHTML)
 *  
 *  @private
 *  @returns {String} HTML content
 */
Xinha.prototype.getInnerHTML = function()
{
  if ( !this._doc.body )
  {
    return '';
  }
  var html = "";
  switch ( this._editMode )
  {
    case "wysiwyg":
      if ( !this.config.fullPage )
      {
        // return this._doc.body.innerHTML;
        html = this._doc.body.innerHTML;
      }
      else
      {
        html = this.doctype + "\n" + this._doc.documentElement.innerHTML;
      }
    break;
    case "textmode" :
      html = this._textArea.value;
    break;
    default:
      alert("Mode <" + this._editMode + "> not defined!");
      return false;
  }

  return html;
};

/** Completely change the HTML inside
 *
 *  @private
 *  @param {String} html new content, should have been run through inwardHtml() first
 */
Xinha.prototype.setHTML = function(html)
{
  if ( !this.config.fullPage )
  {
    this._doc.body.innerHTML = html;
  }
  else
  {
    this.setFullHTML(html);
  }
  this._textArea.value = html;
};

/** sets the given doctype (useful only when config.fullPage is true)
 *  
 *  @private
 *  @param {String} doctype
 */
Xinha.prototype.setDoctype = function(doctype)
{
  this.doctype = doctype;
};

/***************************************************
 *  Category: UTILITY FUNCTIONS
 ***************************************************/

/** Variable used to pass the object to the popup editor window.
 *  @FIXME: Is this in use?
 *  @deprecated 
 *  @private
 *  @type {Object}
 */
Xinha._object = null;

/** Arrays are identified as "object" in typeof calls. Adding this tag to the Array prototype allows to distinguish between the two
 */
Array.prototype.isArray = true;
/** RegExps are identified as "object" in typeof calls. Adding this tag to the RegExp prototype allows to distinguish between the two
 */
RegExp.prototype.isRegExp = true;
/** function that returns a clone of the given object
 *  
 *  @private
 *  @param {Object} obj
 *  @returns {Object} cloned object
 */
Xinha.cloneObject = function(obj)
{
  if ( !obj )
  {
    return null;
  }
  var newObj = obj.isArray ? [] : {};

  // check for function and RegExp objects (as usual, IE is fucked up)
  if ( obj.constructor.toString().match( /\s*function Function\(/ ) || typeof obj == 'function' )
  {
    newObj = obj; // just copy reference to it
  }
  else if (  obj.isRegExp )
  {
    newObj = eval( obj.toString() ); //see no way without eval
  }
  else
  {
    for ( var n in obj )
    {
      var node = obj[n];
      if ( typeof node == 'object' )
      {
        newObj[n] = Xinha.cloneObject(node);
      }
      else
      {
        newObj[n] = node;
      }
    }
  }

  return newObj;
};


/** Extend one class from another, that is, make a sub class.
 *  This manner of doing it was probably first devised by Kevin Lindsey
 *
 *  http://kevlindev.com/tutorials/javascript/inheritance/index.htm
 *
 *  It has subsequently been used in one form or another by various toolkits 
 *  such as the YUI.
 *
 *  I make no claim as to understanding it really, but it works.
 * 
 *  Example Usage:
 *  {{{
 *  -------------------------------------------------------------------------
 
    // =========  MAKING THE INITIAL SUPER CLASS ===========
    
        document.write("<h1>Superclass Creation And Test</h1>");
    
        function Vehicle(name, sound)
        {    
          this.name  = name;
          this.sound = sound
        }
      
        Vehicle.prototype.pressHorn = function()
        {
          document.write(this.name + ': ' + this.sound + '<br/>');
        }
        
        var Bedford  = new Vehicle('Bedford Van', 'Honk Honk');
        Bedford.pressHorn(); // Vehicle::pressHorn() is defined
    
    
    // ========= MAKING A SUBCLASS OF A SUPER CLASS =========
    
        document.write("<h1>Subclass Creation And Test</h1>");
        
        // Make the sub class constructor first
        Car = function(name)
        {
          // This is how we call the parent's constructor, note that
          // we are using Car.parent.... not "this", we can't use this.
          Car.parentConstructor.call(this, name, 'Toot Toot');
        }
        
        // Remember the subclass comes first, then the base class, you are extending
        // Car with the methods and properties of Vehicle.
        Xinha.extend(Car, Vehicle);
        
        var MazdaMx5 = new Car('Mazda MX5');  
        MazdaMx5.pressHorn(); // Car::pressHorn() is inherited from Vehicle::pressHorn()
    
    // =========  ADDING METHODS TO THE SUB CLASS ===========

        document.write("<h1>Add Method to Sub Class And Test</h1>");
        
        Car.prototype.isACar = function()
        {
          document.write(this.name + ": Car::isACar() is implemented, this is a car! <br/>");
          this.pressHorn();
        }
       
        MazdaMx5.isACar(); // Car::isACar() is defined as above
        try      { Bedford.isACar(); } // Vehicle::isACar() is not defined, will throw this exception
        catch(e) { document.write("Bedford: Vehicle::onGettingCutOff() not implemented, this is not a car!<br/>"); }
    
    // =========  EXTENDING A METHOD (CALLING MASKED PARENT METHODS) ===========
    
        document.write("<h1>Extend/Override Inherited Method in Sub Class And Test</h1>");
        
        Car.prototype.pressHorn = function()
        { 
          document.write(this.name + ': I am going to press the horn... <br/>');
          Car.superClass.pressHorn.call(this);        
        }
        MazdaMx5.pressHorn(); // Car::pressHorn()
        Bedford.pressHorn();  // Vehicle::pressHorn()
        
    // =========  MODIFYING THE SUPERCLASS AFTER SUBCLASSING ===========
    
        document.write("<h1>Add New Method to Superclass And Test In Subclass</h1>");  
        
        Vehicle.prototype.startUp = function() { document.write(this.name + ": Vroooom <br/>"); }  
        MazdaMx5.startUp(); // Cars get the prototype'd startUp() also.
        
 *  -------------------------------------------------------------------------
 *  }}}  
 *
 *  @param subclass_constructor (optional)  Constructor function for the subclass
 *  @param superclass Constructor function for the superclass 
 */

Xinha.extend = function(subClass, baseClass) {
   function inheritance() {}
   inheritance.prototype = baseClass.prototype;

   subClass.prototype = new inheritance();
   subClass.prototype.constructor = subClass;
   subClass.parentConstructor = baseClass;
   subClass.superClass = baseClass.prototype;
}

/** Event Flushing
 *  To try and work around memory leaks in the rather broken
 *  garbage collector in IE, Xinha.flushEvents can be called
 *  onunload, it will remove any event listeners (that were added
 *  through _addEvent(s)) and clear any DOM-0 events.
 *  @private
 *
 */
Xinha.flushEvents = function()
{
  var x = 0;
  // @todo : check if Array.prototype.pop exists for every supported browsers
  var e = Xinha._eventFlushers.pop();
  while ( e )
  {
    try
    {
      if ( e.length == 3 )
      {
        Xinha._removeEvent(e[0], e[1], e[2]);
        x++;
      }
      else if ( e.length == 2 )
      {
        e[0]['on' + e[1]] = null;
        e[0]._xinha_dom0Events[e[1]] = null;
        x++;
      }
    }
    catch(ex)
    {
      // Do Nothing
    }
    e = Xinha._eventFlushers.pop();
  }
  
  /* 
    // This code is very agressive, and incredibly slow in IE, so I've disabled it.
    
    if(document.all)
    {
      for(var i = 0; i < document.all.length; i++)
      {
        for(var j in document.all[i])
        {
          if(/^on/.test(j) && typeof document.all[i][j] == 'function')
          {
            document.all[i][j] = null;
            x++;
          }
        }
      }
    }
  */
  
  // alert('Flushed ' + x + ' events.');
};
 /** Holds the events to be flushed
  * @type Array
  */
Xinha._eventFlushers = [];

if ( document.addEventListener )
{
 /** adds an event listener for the specified element and event type
 *  
 *  @public
 *  @see   Xinha#_addEvents
 *  @see   Xinha#addDom0Event
 *  @see   Xinha#prependDom0Event
 *  @param {DomNode}  el the DOM element the event should be attached to 
 *  @param {String}   evname the name of the event to listen for (without leading "on")
 *  @param {function} func the function to be called when the event is fired
 */
  Xinha._addEvent = function(el, evname, func)
  {
    el.addEventListener(evname, func, false);
    Xinha._eventFlushers.push([el, evname, func]);
  };
 
 /** removes an event listener previously added
 *  
 *  @public
 *  @see   Xinha#_removeEvents
 *  @param {DomNode}  el the DOM element the event should be removed from 
 *  @param {String}   evname the name of the event the listener should be removed from (without leading "on")
 *  @param {function} func the function to be removed
 */
  Xinha._removeEvent = function(el, evname, func)
  {
    el.removeEventListener(evname, func, false);
  };
 
 /** stops bubbling of the event, if no further listeners should be triggered
 *  
 *  @public
 *  @param {event} ev the event to be stopped
 */
  Xinha._stopEvent = function(ev)
  {
    if(ev.preventDefault)
    {  
      ev.preventDefault();
    }
    // IE9 now supports addEventListener, but does not support preventDefault.  Sigh
    else
    {
      ev.returnValue = false;
    }
    
    if(ev.stopPropagation)
    {
      ev.stopPropagation();
    }
    // IE9 now supports addEventListener, but does not support stopPropagation.  Sigh
    else
    {
      ev.cancelBubble = true;
    }
  };
}
 /** same as above, for IE
 *  
 */
else if ( document.attachEvent )
{
  Xinha._addEvent = function(el, evname, func)
  {
    el.attachEvent("on" + evname, func);
    Xinha._eventFlushers.push([el, evname, func]);
  };
  Xinha._removeEvent = function(el, evname, func)
  {
    el.detachEvent("on" + evname, func);
  };
  Xinha._stopEvent = function(ev)
  {
    try
    {
      ev.cancelBubble = true;
      ev.returnValue = false;
    }
    catch (ex)
    {
      // Perhaps we could try here to stop the window.event
      // window.event.cancelBubble = true;
      // window.event.returnValue = false;
    }
  };
}
else
{
  Xinha._addEvent = function(el, evname, func)
  {
    alert('_addEvent is not supported');
  };
  Xinha._removeEvent = function(el, evname, func)
  {
    alert('_removeEvent is not supported');
  };
  Xinha._stopEvent = function(ev)
  {
    alert('_stopEvent is not supported');
  };
}
 /** add several events at once to one element
 *  
 *  @public
 *  @see Xinha#_addEvent
 *  @param {DomNode}  el the DOM element the event should be attached to 
 *  @param {Array}    evs the names of the event to listen for (without leading "on")
 *  @param {function} func the function to be called when the event is fired
 */
Xinha._addEvents = function(el, evs, func)
{
  for ( var i = evs.length; --i >= 0; )
  {
    Xinha._addEvent(el, evs[i], func);
  }
};
 /** remove several events at once to from element
 *  
 *  @public
 *  @see Xinha#_removeEvent
 *  @param {DomNode}  el the DOM element the events should be remove from
 *  @param {Array}    evs the names of the events the listener should be removed from (without leading "on")
 *  @param {function} func the function to be removed
 */
Xinha._removeEvents = function(el, evs, func)
{
  for ( var i = evs.length; --i >= 0; )
  {
    Xinha._removeEvent(el, evs[i], func);
  }
};

/** Adds a function that is executed in the moment the DOM is ready, but as opposed to window.onload before images etc. have been loaded
*   http://dean.edwards.name/weblog/2006/06/again/
*   IE part from jQuery
*  @public
*  @author Dean Edwards/Matthias Miller/ John Resig / Diego Perini
*  @param {Function}  func the function to be executed
*  @param {Window}    scope the window that is listened to
*/
Xinha.addOnloadHandler = function (func, scope)
{
 scope = scope ? scope : window;

 var init = function ()
 {
   // quit if this function has already been called
   if (arguments.callee.done) 
   {
     return;
   }
   // flag this function so we don't do the same thing twice
   arguments.callee.done = true;
   // kill the timer
   if (Xinha.onloadTimer)
   {
     clearInterval(Xinha.onloadTimer);
   }

   func();
 };
  if (Xinha.is_ie)
  {
    // ensure firing before onload,
    // maybe late but safe also for iframes
    document.attachEvent("onreadystatechange", function(){
      if ( document.readyState === "complete" ) {
        document.detachEvent( "onreadystatechange", arguments.callee );
        init();
      }
    });
    if ( document.documentElement.doScroll && typeof window.frameElement === "undefined" ) (function(){
      if (arguments.callee.done) return;
      try {
        // If IE is used, use the trick by Diego Perini
        // http://javascript.nwbox.com/IEContentLoaded/
        document.documentElement.doScroll("left");
      } catch( error ) {
        setTimeout( arguments.callee, 0 );
        return;
      }
      // and execute any waiting functions
      init();
    })();
  }
 else if (/applewebkit|KHTML/i.test(navigator.userAgent) ) /* Safari/WebKit/KHTML */
 {
   Xinha.onloadTimer = scope.setInterval(function()
   {
     if (/loaded|complete/.test(scope.document.readyState))
     {
       init(); // call the onload handler
     }
   }, 10);
 }
 else /* for Mozilla/Opera9 */
 {
   scope.document.addEventListener("DOMContentLoaded", init, false);

 }
 Xinha._addEvent(scope, 'load', init); // incase anything went wrong
};

/**
 * Adds a standard "DOM-0" event listener to an element.
 * The DOM-0 events are those applied directly as attributes to
 * an element - eg element.onclick = stuff;
 *
 * By using this function instead of simply overwriting any existing
 * DOM-0 event by the same name on the element it will trigger as well
 * as the existing ones.  Handlers are triggered one after the other
 * in the order they are added.
 *
 * Remember to return true/false from your handler, this will determine
 * whether subsequent handlers will be triggered (ie that the event will
 * continue or be canceled).
 *  
 *  @public
 *  @see Xinha#_addEvent
 *  @see Xinha#prependDom0Event
 *  @param {DomNode}  el the DOM element the event should be attached to 
 *  @param {String}   ev the name of the event to listen for (without leading "on")
 *  @param {function} fn the function to be called when the event is fired
 */

Xinha.addDom0Event = function(el, ev, fn)
{
  Xinha._prepareForDom0Events(el, ev);
  el._xinha_dom0Events[ev].unshift(fn);
};


/** See addDom0Event, the difference is that handlers registered using
 *  prependDom0Event will be triggered before existing DOM-0 events of the
 *  same name on the same element.
 *  
 *  @public
 *  @see Xinha#_addEvent
 *  @see Xinha#addDom0Event
 *  @param {DomNode}  the DOM element the event should be attached to 
 *  @param {String}   the name of the event to listen for (without leading "on")
 *  @param {function} the function to be called when the event is fired
 */

Xinha.prependDom0Event = function(el, ev, fn)
{
  Xinha._prepareForDom0Events(el, ev);
  el._xinha_dom0Events[ev].push(fn);
};

Xinha.getEvent = function(ev)
{
  return ev || window.event;
};
/**
 * Prepares an element to receive more than one DOM-0 event handler
 * when handlers are added via addDom0Event and prependDom0Event.
 *
 * @private
 */
Xinha._prepareForDom0Events = function(el, ev)
{
  // Create a structure to hold our lists of event handlers
  if ( typeof el._xinha_dom0Events == 'undefined' )
  {
    el._xinha_dom0Events = {};
    Xinha.freeLater(el, '_xinha_dom0Events');
  }

  // Create a list of handlers for this event type
  if ( typeof el._xinha_dom0Events[ev] == 'undefined' )
  {
    el._xinha_dom0Events[ev] = [ ];
    if ( typeof el['on'+ev] == 'function' )
    {
      el._xinha_dom0Events[ev].push(el['on'+ev]);
    }

    // Make the actual event handler, which runs through
    // each of the handlers in the list and executes them
    // in the correct context.
    el['on'+ev] = function(event)
    {
      var a = el._xinha_dom0Events[ev];
      // call previous submit methods if they were there.
      var allOK = true;
      for ( var i = a.length; --i >= 0; )
      {
        // We want the handler to be a member of the form, not the array, so that "this" will work correctly
        el._xinha_tempEventHandler = a[i];
        if ( el._xinha_tempEventHandler(event) === false )
        {
          el._xinha_tempEventHandler = null;
          allOK = false;
          break;
        }
        el._xinha_tempEventHandler = null;
      }
      return allOK;
    };

    Xinha._eventFlushers.push([el, ev]);
  }
};

Xinha.prototype.notifyOn = function(ev, fn)
{
  if ( typeof this._notifyListeners[ev] == 'undefined' )
  {
    this._notifyListeners[ev] = [];
    Xinha.freeLater(this, '_notifyListeners');
  }
  this._notifyListeners[ev].push(fn);
};

Xinha.prototype.notifyOf = function(ev, args)
{
  if ( this._notifyListeners[ev] )
  {
    for ( var i = 0; i < this._notifyListeners[ev].length; i++ )
    {
      this._notifyListeners[ev][i](ev, args);
    }
  }
};

/** List of tag names that are defined as block level elements in HTML
 *  
 *  @private
 *  @see Xinha#isBlockElement
 *  @type {String}
 */
Xinha._blockTags = " body form textarea fieldset ul ol dl li div " +
"p h1 h2 h3 h4 h5 h6 quote pre table thead " +
"tbody tfoot tr td th iframe address blockquote title meta link style head ";

/** Checks if one element is in the list of elements that are defined as block level elements in HTML
 *  
 *  @param {DomNode}  el The DOM element to check
 *  @returns {Boolean}
 */
Xinha.isBlockElement = function(el)
{
  return el && el.nodeType == 1 && (Xinha._blockTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
};
/** List of tag names that are allowed to contain a paragraph
 *  
 *  @private
 *  @see Xinha#isParaContainer
 *  @type {String}
 */
Xinha._paraContainerTags = " body td th caption fieldset div ";
/** Checks if one element is in the list of elements that are allowed to contain a paragraph in HTML
 *  
 *  @param {DomNode}  el The DOM element to check
 *  @returns {Boolean}
 */
Xinha.isParaContainer = function(el)
{
  return el && el.nodeType == 1 && (Xinha._paraContainerTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
};


/** These are all the tags for which the end tag is not optional or  forbidden, taken from the list at:
 *   http: www.w3.org/TR/REC-html40/index/elements.html
 *  
 *  @private
 *  @see Xinha#needsClosingTag
 *  @type String
 */
Xinha._closingTags = " a abbr acronym address applet b bdo big blockquote button caption center cite code del dfn dir div dl em fieldset font form frameset h1 h2 h3 h4 h5 h6 i iframe ins kbd label legend map menu noframes noscript object ol optgroup pre q s samp script select small span strike strong style sub sup table textarea title tt u ul var ";

/** Checks if one element is in the list of elements for which the end tag is not optional or  forbidden in HTML
 *  
 *  @param {DomNode}  el The DOM element to check
 *  @returns {Boolean}
 */
Xinha.needsClosingTag = function(el)
{
  return el && el.nodeType == 1 && (Xinha._closingTags.indexOf(" " + el.tagName.toLowerCase() + " ") != -1);
};

/** Performs HTML encoding of some given string (converts HTML special characters to entities)
 *  
 *  @param {String}  str The unencoded input
 *  @returns {String} The encoded output
 */
Xinha.htmlEncode = function(str)
{
  if (!str)
  {
    return '';
  }  if ( typeof str.replace == 'undefined' )
  {
    str = str.toString();
  }
  // we don't need regexp for that, but.. so be it for now.
  str = str.replace(/&/ig, "&amp;");
  str = str.replace(/</ig, "&lt;");
  str = str.replace(/>/ig, "&gt;");
  str = str.replace(/\xA0/g, "&nbsp;"); // Decimal 160, non-breaking-space
  str = str.replace(/\x22/g, "&quot;");
  // \x22 means '"' -- we use hex reprezentation so that we don't disturb
  // JS compressors (well, at least mine fails.. ;)
  return str;
};

/** Strips host-part of URL which is added by browsers to links relative to server root
 *  
 *  @param {String}  string 
 *  @returns {String} 
 */
Xinha.prototype.stripBaseURL = function(string)
{
  if ( this.config.baseHref === null || !this.config.stripBaseHref )
  {
    return string;
  }
  var baseurl = this.config.baseHref.replace(/^(https?:\/\/[^\/]+)(.*)$/, '$1');
  var basere = new RegExp(baseurl);
  return string.replace(basere, "");
};

if (typeof String.prototype.trim != 'function')
{
  /** Removes whitespace from beginning and end of a string. Custom implementation for JS engines that don't support it natively
   *  
   *  @returns {String} 
   */
  String.prototype.trim = function()
  {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  };
}

/** Creates a rgb-style rgb(r,g,b) color from a (24bit) number
 *  
 *  @param {Integer}
 *  @returns {String} rgb(r,g,b) color definition
 */
Xinha._makeColor = function(v)
{
  if ( typeof v != "number" )
  {
    // already in rgb (hopefully); IE doesn't get here.
    return v;
  }
  // IE sends number; convert to rgb.
  var r = v & 0xFF;
  var g = (v >> 8) & 0xFF;
  var b = (v >> 16) & 0xFF;
  return "rgb(" + r + "," + g + "," + b + ")";
};

/** Returns hexadecimal color representation from a number or a rgb-style color.
 *  
 *  @param {String|Integer} v rgb(r,g,b) or 24bit color definition
 *  @returns {String} #RRGGBB color definition
 */
Xinha._colorToRgb = function(v)
{
  if ( !v )
  {
    return '';
  }
  var r,g,b;
  // @todo: why declaring this function here ? This needs to be a public methode of the object Xinha._colorToRgb
  // returns the hex representation of one byte (2 digits)
  function hex(d)
  {
    return (d < 16) ? ("0" + d.toString(16)) : d.toString(16);
  }

  if ( typeof v == "number" )
  {
    // we're talking to IE here
    r = v & 0xFF;
    g = (v >> 8) & 0xFF;
    b = (v >> 16) & 0xFF;
    return "#" + hex(r) + hex(g) + hex(b);
  }

  if ( v.substr(0, 3) == "rgb" )
  {
    // in rgb(...) form -- Mozilla
    var re = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/;
    if ( v.match(re) )
    {
      r = parseInt(RegExp.$1, 10);
      g = parseInt(RegExp.$2, 10);
      b = parseInt(RegExp.$3, 10);
      return "#" + hex(r) + hex(g) + hex(b);
    }
    // doesn't match RE?!  maybe uses percentages or float numbers
    // -- FIXME: not yet implemented.
    return null;
  }

  if ( v.substr(0, 1) == "#" )
  {
    // already hex rgb (hopefully :D )
    return v;
  }

  // if everything else fails ;)
  return null;
};

/** Modal popup dialogs
 *  
 *  @param {String} url URL to the popup dialog
 *  @param {Function} action A function that receives one value; this function will get called 
 *                    after the dialog is closed, with the return value of the dialog.
 *  @param {Mixed} init A variable that is passed to the popup window to pass arbitrary data
 */
Xinha.prototype._popupDialog = function(url, action, init)
{
  Dialog(this.popupURL(url), action, init);
};

/** Creates a path in the form _editor_url + "plugins/" + plugin + "/img/" + file
 *  
 *  @deprecated
 *  @param {String} file Name of the image
 *  @param {String} plugin optional If omitted, simply _editor_url + file is returned 
 *  @returns {String}
 */
Xinha.prototype.imgURL = function(file, plugin)
{
  if ( typeof plugin == "undefined" )
  {
    return _editor_url + file;
  }
  else
  {
    return Xinha.getPluginDir(plugin) + "/img/" + file;
  }
};
/** Creates a path
 *  
 *  @deprecated
 *  @param {String} file Name of the popup
 *  @returns {String}
 */
Xinha.prototype.popupURL = function(file)
{
  var url = "";
  if ( file.match(/^plugin:\/\/(.*?)\/(.*)/) )
  {
    var plugin = RegExp.$1;
    var popup = RegExp.$2;
    if ( !/\.(html?|php)$/.test(popup) )
    {
      popup += ".html";
    }
    url = Xinha.getPluginDir(plugin) + "/popups/" + popup;
  }
  else if ( file.match(/^\/.*?/) || file.match(/^https?:\/\//))
  {
    url = file;
  }
  else
  {
    url = _editor_url + this.config.popupURL + file;
  }
  return url;
};



/** FIX: Internet Explorer returns an item having the _name_ equal to the given
 * id, even if it's not having any id.  This way it can return a different form
 * field, even if it's not a textarea.  This workarounds the problem by
 * specifically looking to search only elements having a certain tag name.
 * @param {String} tag The tag name to limit the return to
 * @param {String} id
 * @returns {DomNode}
 */
Xinha.getElementById = function(tag, id)
{
  var el, i, objs = document.getElementsByTagName(tag);
  for ( i = objs.length; --i >= 0 && (el = objs[i]); )
  {
    if ( el.id == id )
    {
      return el;
    }
  }
  return null;
};


/** Use some CSS trickery to toggle borders on tables 
 *	@returns {Boolean} always true
 */

Xinha.prototype._toggleBorders = function()
{
  var tables = this._doc.getElementsByTagName('TABLE');
  if ( tables.length !== 0 )
  {
   if ( !this.borders )
   {    
    this.borders = true;
   }
   else
   {
     this.borders = false;
   }

   for ( var i=0; i < tables.length; i++ )
   {
     if ( this.borders )
     {
        Xinha._addClass(tables[i], 'htmtableborders');
     }
     else
     {
       Xinha._removeClass(tables[i], 'htmtableborders');
     }
   }
  }
  return true;
};
/** Adds the styles for table borders to the iframe during generation
 *  
 *  @private
 *  @see Xinha#stripCoreCSS
 *  @param {String} html optional  
 *  @returns {String} html HTML with added styles or only styles if html omitted
 */
Xinha.addCoreCSS = function(html)
{
    var coreCSS = "<style title=\"XinhaInternalCSS\" type=\"text/css\">" +
    ".htmtableborders, .htmtableborders td, .htmtableborders th {border : 1px dashed lightgrey ! important;}\n" +
    "html, body { border: 0px; } \n" +
    "body { background-color: #ffffff; } \n" +
    "img, hr { cursor: default } \n" +
    "</style>\n";
    
    if( html && /<head>/i.test(html))
    {
      return html.replace(/<head>/i, '<head>' + coreCSS);      
    }
    else if ( html)
    {
      return coreCSS + html;
    }
    else
    {
      return coreCSS;
    }
};
/** Allows plugins to add a stylesheet for internal use to the edited document that won't appear in the HTML output
 *  
 *  @see Xinha#stripCoreCSS
 *  @param {String} stylesheet URL of the styleshett to be added
 */
Xinha.prototype.addEditorStylesheet = function (stylesheet)
{
    var style = this._doc.createElement("link");
    style.rel = 'stylesheet';
    style.type = 'text/css';
    style.title = 'XinhaInternalCSS';
    style.href = stylesheet;
    this._doc.getElementsByTagName("HEAD")[0].appendChild(style);
};
/** Remove internal styles
 *  
 *  @private
 *  @see Xinha#addCoreCSS
 *  @param {String} html 
 *  @returns {String} 
 */
Xinha.stripCoreCSS = function(html)
{
  return html.replace(/<style[^>]+title="XinhaInternalCSS"(.|\n)*?<\/style>/ig, '').replace(/<link[^>]+title="XinhaInternalCSS"(.|\n)*?>/ig, ''); 
};
/** Removes one CSS class (that is one of possible more parts 
 *   separated by spaces) from a given element
 *  
 *  @see Xinha#_removeClasses
 *  @param {DomNode}  el The DOM element the class will be removed from
 *  @param {String}   className The class to be removed
 */
Xinha._removeClass = function(el, className)
{
  if ( ! ( el && el.className ) )
  {
    return;
  }
  var cls = el.className.split(" ");
  var ar = [];
  for ( var i = cls.length; i > 0; )
  {
    if ( cls[--i] != className )
    {
      ar[ar.length] = cls[i];
    }
  }
  el.className = ar.join(" ");
};
/** Adds one CSS class  to a given element (that is, it expands its className property by the given string,
 *  separated by a space)
 *  
 *  @see Xinha#addClasses
 *  @param {DomNode}  el The DOM element the class will be added to
 *  @param {String}   className The class to be added
 */
Xinha._addClass = function(el, className)
{
  // remove the class first, if already there
  Xinha._removeClass(el, className);
  el.className += " " + className;
};

/** Adds CSS classes  to a given element (that is, it expands its className property by the given string,
 *  separated by a space, thereby checking that no class is doubly added)
 *  
 *  @see Xinha#addClass
 *  @param {DomNode}  el The DOM element the classes will be added to
 *  @param {String}   classes The classes to be added
 */
Xinha.addClasses = function(el, classes)
{
  if ( el !== null )
  {
    var thiers = el.className.trim().split(' ');
    var ours   = classes.split(' ');
    for ( var x = 0; x < ours.length; x++ )
    {
      var exists = false;
      for ( var i = 0; exists === false && i < thiers.length; i++ )
      {
        if ( thiers[i] == ours[x] )
        {
          exists = true;
        }
      }
      if ( exists === false )
      {
        thiers[thiers.length] = ours[x];
      }
    }
    el.className = thiers.join(' ').trim();
  }
};

/** Removes CSS classes (that is one or more of possibly several parts 
 *   separated by spaces) from a given element
 *  
 *  @see Xinha#_removeClasses
 *  @param {DomNode}  el The DOM element the class will be removed from
 *  @param {String}   className The class to be removed
 */
Xinha.removeClasses = function(el, classes)
{
  var existing    = el.className.trim().split();
  var new_classes = [];
  var remove      = classes.trim().split();

  for ( var i = 0; i < existing.length; i++ )
  {
    var found = false;
    for ( var x = 0; x < remove.length && !found; x++ )
    {
      if ( existing[i] == remove[x] )
      {
        found = true;
      }
    }
    if ( !found )
    {
      new_classes[new_classes.length] = existing[i];
    }
  }
  return new_classes.join(' ');
};

/** Alias of Xinha._addClass()
 *  @see Xinha#_addClass
 */
Xinha.addClass       = Xinha._addClass;
/** Alias of Xinha.Xinha._removeClass()
 *  @see Xinha#_removeClass
 */
Xinha.removeClass    = Xinha._removeClass;
/** Alias of Xinha.addClasses()
 *  @see Xinha#addClasses
 */
Xinha._addClasses    = Xinha.addClasses;
/** Alias of Xinha.removeClasses()
 *  @see Xinha#removeClasses
 */
Xinha._removeClasses = Xinha.removeClasses;

/** Checks if one element has set the given className
 *  
 *  @param {DomNode}  el The DOM element to check
 *  @param {String}   className The class to be looked for
 *  @returns {Boolean}
 */
Xinha._hasClass = function(el, className)
{
  if ( ! ( el && el.className ) )
  {
    return false;
  }
  var cls = el.className.split(" ");
  for ( var i = cls.length; i > 0; )
  {
    if ( cls[--i] == className )
    {
      return true;
    }
  }
  return false;
};

/**
 * Use XMLHTTPRequest to post some data back to the server and do something
 * with the response (asyncronously!), this is used by such things as the tidy
 * functions
 * @param {String} url The address for the HTTPRequest
 * @param {Object} data The data to be passed to the server like {name:"value"}
 * @param {Function} success A function that is called when an answer is
 *                           received from the server with the responseText as argument.
 * @param {Function} failure A function that is called when we fail to receive
 *                           an answer from the server. We pass it the request object.
 */
 
/** mod_security (an apache module which scans incoming requests for potential hack attempts)
 *  has a rule which triggers when it gets an incoming Content-Type with a charset
 *  see ticket:1028 to try and work around this, if we get a failure in a postback
 *  then Xinha._postback_send_charset will be set to false and the request tried again (once)
 *  @type Boolean
 *  @private
 */ 
// 
// 
// 
Xinha._postback_send_charset = true;
/** Use XMLHTTPRequest to send some some data to the server and do something
 *  with the getback (asyncronously!)
 * @param {String} url The address for the HTTPRequest
 * @param {Function} success A function that is called when an answer is
 *                           received from the server with the responseText as argument.
 * @param {Function} failure A function that is called when we fail to receive
 *                           an answer from the server. We pass it the request object.
 */
Xinha._postback = function(url, data, success, failure)
{
  var req = null;
  req = Xinha.getXMLHTTPRequestObject();

  var content = '';
  if (typeof data == 'string')
  {
    content = data;
  }
  else if(typeof data == "object")
  {
    for ( var i in data )
    {
      content += (content.length ? '&' : '') + i + '=' + encodeURIComponent(data[i]);
    }
  }

  function callBack()
  {
    if ( req.readyState == 4 )
    {
      if ( ((req.status / 100) == 2) || Xinha.isRunLocally && req.status === 0 )
      {
        if ( typeof success == 'function' )
        {
          success(req.responseText, req);
        }
      }
      else if(Xinha._postback_send_charset)
      {        
        Xinha._postback_send_charset = false;
        Xinha._postback(url,data,success, failure);
      }
      else if (typeof failure == 'function')
      {
        failure(req);
      }
      else
      {
        alert('An error has occurred: ' + req.statusText + '\nURL: ' + url);
      }
    }
  }

  req.onreadystatechange = callBack;

  req.open('POST', url, true);
  req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'+(Xinha._postback_send_charset ? '; charset=UTF-8' : ''));

  req.send(content);
};

/** Use XMLHTTPRequest to receive some data from the server and do something
 * with the it (asyncronously!)
 * @param {String} url The address for the HTTPRequest
 * @param {Function} success A function that is called when an answer is
 *                           received from the server with the responseText as argument.
 * @param {Function} failure A function that is called when we fail to receive
 *                           an answer from the server. We pass it the request object.
 */
Xinha._getback = function(url, success, failure)
{
  var req = null;
  req = Xinha.getXMLHTTPRequestObject();

  function callBack()
  {
    if ( req.readyState == 4 )
    {
      if ( ((req.status / 100) == 2) || Xinha.isRunLocally && req.status === 0 )
      {
        success(req.responseText, req);
      }
      else if (typeof failure == 'function')
      {
        failure(req);
      }
      else
      {
        alert('An error has occurred: ' + req.statusText + '\nURL: ' + url);
      }
    }
  }

  req.onreadystatechange = callBack;
  req.open('GET', url, true);
  req.send(null);
};

Xinha.ping = function(url, successHandler, failHandler)
{
  var req = null;
  req = Xinha.getXMLHTTPRequestObject();

  function callBack()
  {
    if ( req.readyState == 4 )
    {
      if ( ((req.status / 100) == 2) || Xinha.isRunLocally && req.status === 0 )
      {
        if (successHandler) 
        {
          successHandler(req);
        }
      }
      else
      {
        if (failHandler) 
        {
          failHandler(req);
        }
      }
    }
  }

  // Opera seems to have some problems mixing HEAD requests with GET requests.
  // The GET is slower, so it's a net slowdown for Opera, but it keeps things
  // from breaking.
  var method = 'GET';
  req.onreadystatechange = callBack;
  req.open(method, url, true);
  req.send(null);
};

/** Use XMLHTTPRequest to receive some data from the server syncronously
 *  @param {String} url The address for the HTTPRequest
 */
Xinha._geturlcontent = function(url, returnXML)
{
  var req = null;
  req = Xinha.getXMLHTTPRequestObject();

  // Synchronous!
  req.open('GET', url, false);
  req.send(null);
  if ( ((req.status / 100) == 2) || Xinha.isRunLocally && req.status === 0 )
  {
    return (returnXML) ? req.responseXML : req.responseText;
  }
  else
  {
    return '';
  }
};


/** Use XMLHTTPRequest to send some some data to the server and return the result synchronously
 *
 * @param {String} url The address for the HTTPRequest
 * @param data the data to send, streing or array
 */
Xinha._posturlcontent = function(url, data, returnXML)
{
  var req = null;
  req = Xinha.getXMLHTTPRequestObject();

  var content = '';
  if (typeof data == 'string')
  {
    content = data;
  }
  else if(typeof data == "object")
  {
    for ( var i in data )
    {
      content += (content.length ? '&' : '') + i + '=' + encodeURIComponent(data[i]);
    }
  }

  req.open('POST', url, false);    
  req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'+(Xinha._postback_send_charset ? '; charset=UTF-8' : ''));
  req.send(content);
  
  if ( ((req.status / 100) == 2) || Xinha.isRunLocally && req.status === 0 )
  {
    return (returnXML) ? req.responseXML : req.responseText;
  }
  else
  {
    return '';
  }
  
};
// Unless somebody already has, make a little function to debug things

if (typeof dumpValues == 'undefined') 
{
  dumpValues = function(o)
  {
    var s = '';
    for (var prop in o) 
    {
      if (window.console && typeof window.console.log == 'function') 
      {
        if (typeof console.firebug != 'undefined') 
        {
          console.log(o);
        }
        else 
        {
          console.log(prop + ' = ' + o[prop] + '\n');
        }
      }
      else
      {
        s += prop + ' = ' + o[prop] + '\n';
      }
    }
    if (s) 
    {
      if (document.getElementById('errors'))
      {
        document.getElementById('errors').value += s;
      }
      else
      {
        var x = window.open("", "debugger");
        x.document.write('<pre>' + s + '</pre>');
      }

    }
  };
}
if ( !Array.prototype.contains )
{
  /** Walks through an array and checks if the specified item exists in it
  * @param {String} needle The string to search for
  * @returns {Boolean} True if item found, false otherwise 
  */
  Array.prototype.contains = function(needle)
  {
    var haystack = this;
    for ( var i = 0; i < haystack.length; i++ )
    {
      if ( needle == haystack[i] )
      {
        return true;
      }
    }
    return false;
  };
}

if ( !Array.prototype.indexOf )
{
  /** Walks through an array and, if the specified item exists in it, returns the position
  * @param {String} needle The string to search for
  * @returns {Integer|-1} Index position if item found, -1 otherwise (same as built in js)
  */
  Array.prototype.indexOf = function(needle)
  {
    var haystack = this;
    for ( var i = 0; i < haystack.length; i++ )
    {
      if ( needle == haystack[i] )
      {
        return i;
      }
    }
    return -1;
  };
}
if ( !Array.prototype.append )
{
  /** Adds an item to an array
   * @param {Mixed} a Item to add
   * @returns {Array} The array including the newly added item
   */
  Array.prototype.append  = function(a)
  {
    for ( var i = 0; i < a.length; i++ )
    {
      this.push(a[i]);
    }
    return this;
  };
}
/** Executes a provided function once per array element.
 *  Custom implementation for JS engines that don't support it natively
 * @source http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Global_Objects/Array/ForEach
 * @param {Function} fn Function to execute for each element
 * @param {Object} thisObject Object to use as this when executing callback. 
 */
if (!Array.prototype.forEach)
{
  Array.prototype.forEach = function(fn /*, thisObject*/)
  {
    var len = this.length;
    if (typeof fn != "function")
    {
      throw new TypeError();
    }

    var thisObject = arguments[1];
    for (var i = 0; i < len; i++)
    {
      if (i in this)
      {
        fn.call(thisObject, this[i], i, this);
      }
    }
  };
}
/** Returns all elements within a given class name inside an element
 * @type Array
 * @param {DomNode|document} el wherein to search
 * @param {Object} className
 */
Xinha.getElementsByClassName = function(el,className)
{
  if (el.getElementsByClassName)
  {
    return Array.prototype.slice.call(el.getElementsByClassName(className));
  }
  else
  {
    var els = el.getElementsByTagName('*');
    var result = [];
    var classNames;
    for (var i=0;i<els.length;i++)
    {
      classNames = els[i].className.split(' ');
      if (classNames.contains(className)) 
      {
        result.push(els[i]);
      }
    }
    return result;
  }
};

/** Returns true if all elements of <em>a2</em> are also contained in <em>a1</em> (at least I think this is what it does)
* @param {Array} a1
* @param {Array} a2
* @returns {Boolean}
*/
Xinha.arrayContainsArray = function(a1, a2)
{
  var all_found = true;
  for ( var x = 0; x < a2.length; x++ )
  {
    var found = false;
    for ( var i = 0; i < a1.length; i++ )
    {
      if ( a1[i] == a2[x] )
      {
        found = true;
        break;
      }
    }
    if ( !found )
    {
      all_found = false;
      break;
    }
  }
  return all_found;
};
/** Walks through an array and applies a filter function to each item
* @param {Array} a1 The array to filter
* @param {Function} filterfn If this function returns true, the item is added to the new array
* @returns {Array} Filtered array
*/
Xinha.arrayFilter = function(a1, filterfn)
{
  var new_a = [ ];
  for ( var x = 0; x < a1.length; x++ )
  {
    if ( filterfn(a1[x]) )
    {
      new_a[new_a.length] = a1[x];
    }
  }
  return new_a;
};
/** Converts a Collection object to an array 
* @param {Collection} collection The array to filter
* @returns {Array} Array containing the item of collection
*/
Xinha.collectionToArray = function(collection)
{
  try
  {
    return collection.length ? Array.prototype.slice.call(collection) : []; //Collection to Array
  }
  catch(e)
  {
    // In certain implementations (*cough* IE), you can't call slice on a
    // collection.  We'll fallback to using the simple, non-native iterative
    // approach.
  }

  var array = [ ];
  for ( var i = 0; i < collection.length; i++ )
  {
    array.push(collection.item(i));
  }
  return array;
};

/** Index for Xinha.uniq function 
*	@private
*/
Xinha.uniq_count = 0;
/** Returns a string that is unique on the page
*	@param {String} prefix This string is prefixed to a running number
*   @returns {String}
*/
Xinha.uniq = function(prefix)
{
  return prefix + Xinha.uniq_count++;
};

// New language handling functions

/** Load a language file.
 *  This function should not be used directly, Xinha._lc will use it when necessary.
 *  @private
 *  @param {String} context Case sensitive context name, eg 'Xinha', 'TableOperations', ...
 *  @returns {Object}
 */
Xinha._loadlang = function(context,url)
{
  var lang;
  
  if ( typeof _editor_lcbackend == "string" )
  {
    //use backend
    url = _editor_lcbackend;
    url = url.replace(/%lang%/, _editor_lang);
    url = url.replace(/%context%/, context);
  }
  else if (!url)
  {
    //use internal files
    if ( context != 'Xinha')
    {
      url = Xinha.getPluginDir(context)+"/lang/"+_editor_lang+".js";
    }
    else
    {
      Xinha.setLoadingMessage("Loading language");
      url = _editor_url+"lang/"+_editor_lang+".js";
    }
  }

  var langData = Xinha._geturlcontent(url);
  if ( langData !== "" )
  {
    try
    {
      eval('lang = ' + langData);
    }
    catch(ex)
    {
      alert('Error reading Language-File ('+url+'):\n'+Error.toString());
      lang = {};
    }
  }
  else
  {
    lang = {};
  }

  return lang;
};

/** Return a localised string.
 * @param {String} string English language string. It can also contain variables in the form "Some text with $variable=replaced text$". 
 *                  This replaces $variable in "Some text with $variable" with "replaced text"
 * @param {String} context   Case sensitive context name, eg 'Xinha' (default), 'TableOperations'...
 * @param {Object} replace   Replace $variables in String, eg {foo: 'replaceText'} ($foo in string will be replaced by replaceText)
 */
Xinha._lc = function(string, context, replace)
{
  var url,ret;
  if (typeof context == 'object' && context.url && context.context)
  {
    url = context.url + _editor_lang + ".js";
    context = context.context;
  }

  var m = null;
  if (typeof string == 'string') 
  {
    m = string.match(/\$(.*?)=(.*?)\$/g);
  }
  if (m) 
  {
    if (!replace) 
    {
      replace = {};
    }
    for (var i = 0;i<m.length;i++)
    {
      var n = m[i].match(/\$(.*?)=(.*?)\$/);
      replace[n[1]] = n[2];
      string = string.replace(n[0],'$'+n[1]);
    }
  }
  if ( _editor_lang == "en" )
  {
    if ( typeof string == 'object' && string.string )
    {
      ret = string.string;
    }
    else
    {
      ret = string;
    }
  }
  else
  {
    if ( typeof Xinha._lc_catalog == 'undefined' )
    {
      Xinha._lc_catalog = [ ];
    }

    if ( typeof context == 'undefined' )
    {
      context = 'Xinha';
    }

    if ( typeof Xinha._lc_catalog[context] == 'undefined' )
    {
      Xinha._lc_catalog[context] = Xinha._loadlang(context,url);
    }

    var key;
    if ( typeof string == 'object' && string.key )
    {
      key = string.key;
    }
    else if ( typeof string == 'object' && string.string )
    {
      key = string.string;
    }
    else
    {
      key = string;
    }

    if ( typeof Xinha._lc_catalog[context][key] == 'undefined' )
    {
      if ( context=='Xinha' )
      {
        // Indicate it's untranslated
        if ( typeof string == 'object' && string.string )
        {
          ret = string.string;
        }
        else
        {
          ret = string;
        }
      }
      else
      {
        //if string is not found and context is not Xinha try if it is in Xinha
        return Xinha._lc(string, 'Xinha', replace);
      }
    }
    else
    {
      ret = Xinha._lc_catalog[context][key];
    }
  }

  if ( typeof string == 'object' && string.replace )
  {
    replace = string.replace;
  }
  if ( typeof replace != "undefined" )
  {
    for ( i in replace )
    {
      ret = ret.replace('$'+i, replace[i]);
    }
  }

  return ret;
};
/** Walks through the children of a given element and checks if any of the are visible (= not display:none)
 * @param {DomNode} el 
 * @returns {Boolean} 
 */
Xinha.hasDisplayedChildren = function(el)
{
  var children = el.childNodes;
  for ( var i = 0; i < children.length; i++ )
  {
    if ( children[i].tagName )
    {
      if ( children[i].style.display != 'none' )
      {
        return true;
      }
    }
  }
  return false;
};

/** Load a javascript file by inserting it in the HEAD tag and eventually call a function when loaded
 *
 *  Note that this method cannot be abstracted into browser specific files
 *  because this method LOADS the browser specific files.  Hopefully it should work for most
 *  browsers as it is.
 *
 * @param {String} url               Source url of the file to load
 * @param {Object} callback optional Callback function to launch once ready 
 * @param {Object} scope    optional Application scope for the callback function
 * @param {Object} bonus    optional Arbitrary object send as a param to the callback function
 */
Xinha._loadback = function(url, callback, scope, bonus)
{  
  if ( document.getElementById(url) )
  {
    return true;
  }
  var t = !Xinha.is_ie ? "onload" : 'onreadystatechange';
  var s = document.createElement("script");
  s.type = "text/javascript";
  s.src = url;
  s.id = url;
  if ( callback )
  {
    s[t] = function()
    {      
      if (Xinha.is_ie && (!/loaded|complete/.test(window.event.srcElement.readyState)))
      {
        return;
      }
      
      callback.call(scope ? scope : this, bonus);
      s[t] = null;
    };
  }
  document.getElementsByTagName("head")[0].appendChild(s);
  return false;
};

/** Xinha's main loading function (see NewbieGuide)
 * @param {Array} editor_names
 * @param {Xinha.Config} default_config
 * @param {Array} plugin_names
 * @returns {Object} An object that contains references to all created editors indexed by the IDs of the textareas 
 */
Xinha.makeEditors = function(editor_names, default_config, plugin_names)
{
  if (!Xinha.isSupportedBrowser) 
  {
    return;
  }
  
  if ( typeof default_config == 'function' )
  {
    default_config = default_config();
  }

  var editors = {};
  var textarea;
  for ( var x = 0; x < editor_names.length; x++ )
  {
    if ( typeof editor_names[x] == 'string' ) // the regular case, an id of a textarea
    {
      textarea = Xinha.getElementById('textarea', editor_names[x] );
      if (!textarea) // the id may be specified for a textarea that is maybe on another page; we simply skip it and go on
      {
        editor_names[x] = null;
        continue;
      }
    }
	 // make it possible to pass a reference instead of an id, for example from  document.getElementsByTagName('textarea')
    else if ( typeof editor_names[x] == 'object' && editor_names[x].tagName && editor_names[x].tagName.toLowerCase() == 'textarea' )
    {
      textarea =  editor_names[x];
      if ( !textarea.id ) // we'd like to have the textarea have an id
      {
        textarea.id = 'xinha_id_' + x;
      } 
    }
    var editor = new Xinha(textarea, Xinha.cloneObject(default_config));
    editor.registerPlugins(plugin_names);
    editors[textarea.id] = editor;
  }
  return editors;
};
/** Another main loading function (see NewbieGuide)
 * @param {Object} editors As returned by Xinha.makeEditors()
 */
Xinha.startEditors = function(editors)
{
  if (!Xinha.isSupportedBrowser) 
  {
    return;
  }
  
  for ( var i in editors )
  {
    if ( editors[i].generate )
    {
      editors[i].generate();
    }
  }
};
/** Registers the loaded plugins with the editor
 * @private
 * @param {Array} plugin_names
 */
Xinha.prototype.registerPlugins = function(plugin_names)
{
  if (!Xinha.isSupportedBrowser) 
  {
    return;
  }
  
  if ( plugin_names )
  {
    for ( var i = 0; i < plugin_names.length; i++ )
    {
      this.setLoadingMessage(Xinha._lc('Register plugin $plugin', 'Xinha', {'plugin': plugin_names[i]}));
      this.registerPlugin(plugin_names[i]);
    }
  }
};

/** Utility function to base64_encode some arbitrary data, uses the builtin btoa() if it exists (Moz) 
*  @param {String} input
*  @returns {String}
*/
Xinha.base64_encode = function(input)
{
  var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  var output = "";
  var chr1, chr2, chr3;
  var enc1, enc2, enc3, enc4;
  var i = 0;

  do
  {
    chr1 = input.charCodeAt(i++);
    chr2 = input.charCodeAt(i++);
    chr3 = input.charCodeAt(i++);

    enc1 = chr1 >> 2;
    enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
    enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
    enc4 = chr3 & 63;

    if ( isNaN(chr2) )
    {
      enc3 = enc4 = 64;
    }
    else if ( isNaN(chr3) )
    {
      enc4 = 64;
    }

    output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
  } while ( i < input.length );

  return output;
};

/** Utility function to base64_decode some arbitrary data, uses the builtin atob() if it exists (Moz)
 *  @param {String} input
 *  @returns {String}
 */
Xinha.base64_decode = function(input)
{
  var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  var output = "";
  var chr1, chr2, chr3;
  var enc1, enc2, enc3, enc4;
  var i = 0;

  // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
  input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

  do
  {
    enc1 = keyStr.indexOf(input.charAt(i++));
    enc2 = keyStr.indexOf(input.charAt(i++));
    enc3 = keyStr.indexOf(input.charAt(i++));
    enc4 = keyStr.indexOf(input.charAt(i++));

    chr1 = (enc1 << 2) | (enc2 >> 4);
    chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
    chr3 = ((enc3 & 3) << 6) | enc4;

    output = output + String.fromCharCode(chr1);

    if ( enc3 != 64 )
    {
      output = output + String.fromCharCode(chr2);
    }
    if ( enc4 != 64 )
    {
      output = output + String.fromCharCode(chr3);
    }
  } while ( i < input.length );

  return output;
};
/** Removes a node from the DOM
 *  @param {DomNode} el The element to be removed
 *  @returns {DomNode} The removed element
 */
Xinha.removeFromParent = function(el)
{
  if ( !el.parentNode )
  {
    return;
  }
  var pN = el.parentNode;
  return pN.removeChild(el);
};
/** Checks if some element has a parent node
 *  @param {DomNode} el 
 *  @returns {Boolean}
 */
Xinha.hasParentNode = function(el)
{
  if ( el.parentNode )
  {
    // When you remove an element from the parent in IE it makes the parent
    // of the element a document fragment.  Moz doesn't.
    if ( el.parentNode.nodeType == 11 )
    {
      return false;
    }
    return true;
  }

  return false;
};
/** Determines if a given element has a given attribute.  IE<8 doesn't support it nativly */
Xinha.hasAttribute = function(el,at)
{
  if(typeof el.hasAttribute == 'undefined')
  {
    var node = el.getAttributeNode(at);
    return !!(node && (node.specified || node.nodeValue));
  }
  
  return el.hasAttribute(at);
}

/** Detect the size of visible area
 *  @param {Window} scope optional When calling from a popup window, pass its window object to get the values of the popup
 *  @returns {Object} Object with Integer properties x and y
 */
Xinha.viewportSize = function(scope)
{
  scope = (scope) ? scope : window;
  var x,y;
  if (scope.innerHeight) // all except Explorer
  {
    x = scope.innerWidth;
    y = scope.innerHeight;
  }
  else if (scope.document.documentElement && scope.document.documentElement.clientHeight)
  // Explorer 6 Strict Mode
  {
    x = scope.document.documentElement.clientWidth;
    y = scope.document.documentElement.clientHeight;
  }
  else if (scope.document.body) // other Explorers
  {
    x = scope.document.body.clientWidth;
    y = scope.document.body.clientHeight;
  }
  return {'x':x,'y':y};
};
/** Detect the size of the whole document
 *  @param {Window} scope optional When calling from a popup window, pass its window object to get the values of the popup
 *  @returns {Object} Object with Integer properties x and y
 */
Xinha.pageSize = function(scope)
{
  scope = (scope) ? scope : window;
  var x,y;
 
  var test1 = scope.document.body.scrollHeight; //IE Quirks
  var test2 = scope.document.documentElement.scrollHeight; // IE Standard + Moz Here quirksmode.org errs! 

  if (test1 > test2) 
  {
    x = scope.document.body.scrollWidth;
    y = scope.document.body.scrollHeight;
  }
  else
  {
    x = scope.document.documentElement.scrollWidth;
    y = scope.document.documentElement.scrollHeight;
  }  
  return {'x':x,'y':y};
};
/** Detect the current scroll position
 *  @param {Window} scope optional When calling from a popup window, pass its window object to get the values of the popup
 *  @returns {Object} Object with Integer properties x and y
 */
Xinha.prototype.scrollPos = function(scope)
{
  scope = (scope) ? scope : window;
  var x,y;
  if (typeof scope.pageYOffset != 'undefined') // all except Explorer
  {
    x = scope.pageXOffset;
    y = scope.pageYOffset;
  }
  else if (scope.document.documentElement && typeof document.documentElement.scrollTop != 'undefined')
    // Explorer 6 Strict
  {
    x = scope.document.documentElement.scrollLeft;
    y = scope.document.documentElement.scrollTop;
  }
  else if (scope.document.body) // all other Explorers
  {
    x = scope.document.body.scrollLeft;
    y = scope.document.body.scrollTop;
  }
  return {'x':x,'y':y};
};

/** Calculate the top and left pixel position of an element in the DOM.
 *  @param  {DomNode} element HTML Element
 *  @returns {Object} Object with Integer properties top and left
 */
 
Xinha.getElementTopLeft = function(element) 
{
  var curleft = 0;
  var curtop =  0;
  if (element.offsetParent) 
  {
    curleft = element.offsetLeft;
    curtop = element.offsetTop;
    while (element = element.offsetParent) 
    {
      curleft += element.offsetLeft;
      curtop += element.offsetTop;
    }
  }
  return { top:curtop, left:curleft };
};
/** Find left pixel position of an element in the DOM.
 *  @param  {DomNode} element HTML Element
 *  @returns {Integer} 
 */
Xinha.findPosX = function(obj)
{
  var curleft = 0;
  if ( obj.offsetParent )
  {
    return Xinha.getElementTopLeft(obj).left;
  }
  else if ( obj.x )
  {
    curleft += obj.x;
  }
  return curleft;
};
/** Find top pixel position of an element in the DOM.
 *  @param  {DomNode} element HTML Element
 *  @returns {Integer} 
 */
Xinha.findPosY = function(obj)
{
  var curtop = 0;
  if ( obj.offsetParent )
  {
    return Xinha.getElementTopLeft(obj).top;    
  }
  else if ( obj.y )
  {
    curtop += obj.y;
  }
  return curtop;
};

Xinha.createLoadingMessages = function(xinha_editors)
{
  if ( Xinha.loadingMessages || !Xinha.isSupportedBrowser ) 
  {
    return;
  }
  Xinha.loadingMessages = [];
  
  for (var i=0;i<xinha_editors.length;i++)
  {
    if (!document.getElementById(xinha_editors[i])) 
    {
      continue;
    }
    Xinha.loadingMessages.push(Xinha.createLoadingMessage(Xinha.getElementById('textarea', xinha_editors[i])));
  }
};

Xinha.createLoadingMessage = function(textarea,text)
{ 
  if ( document.getElementById("loading_" + textarea.id) || !Xinha.isSupportedBrowser)
  {
    return;
  }
  // Create and show the main loading message and the sub loading message for details of loading actions
  // global element
  var loading_message = document.createElement("div");
  loading_message.id = "loading_" + textarea.id;
  loading_message.className = "loading";
  
  loading_message.style.left = (Xinha.findPosX(textarea) + textarea.offsetWidth / 2) - 106 +  'px';
  loading_message.style.top = (Xinha.findPosY(textarea) + textarea.offsetHeight / 2) - 50 +  'px';
  // main static message
  var loading_main = document.createElement("div");
  loading_main.className = "loading_main";
  loading_main.id = "loading_main_" + textarea.id;
  loading_main.appendChild(document.createTextNode(Xinha._lc("Loading in progress. Please wait!")));
  // sub dynamic message
  var loading_sub = document.createElement("div");
  loading_sub.className = "loading_sub";
  loading_sub.id = "loading_sub_" + textarea.id;
  text = text ? text : Xinha._lc("Loading Core");
  loading_sub.appendChild(document.createTextNode(text));
  loading_message.appendChild(loading_main);
  loading_message.appendChild(loading_sub);
  document.body.appendChild(loading_message);
  
  Xinha.freeLater(loading_message);
  Xinha.freeLater(loading_main);
  Xinha.freeLater(loading_sub);
  
  return loading_sub;
};

Xinha.prototype.setLoadingMessage = function(subMessage, mainMessage)
{
  if ( !document.getElementById("loading_sub_" + this._textArea.id) )
  {
    return;
  }
  document.getElementById("loading_main_" + this._textArea.id).innerHTML = mainMessage ? mainMessage : Xinha._lc("Loading in progress. Please wait!");
  document.getElementById("loading_sub_" + this._textArea.id).innerHTML = subMessage;
};

Xinha.setLoadingMessage = function(string)
{
  if (!Xinha.loadingMessages) 
  {
    return;
  }
  for ( var i = 0; i < Xinha.loadingMessages.length; i++ )
  {
    Xinha.loadingMessages[i].innerHTML = string;
  }
};

Xinha.prototype.removeLoadingMessage = function()
{
  if (document.getElementById("loading_" + this._textArea.id) )
  {
   document.body.removeChild(document.getElementById("loading_" + this._textArea.id));
  }
};

Xinha.removeLoadingMessages = function(xinha_editors)
{
  for (var i=0;i< xinha_editors.length;i++)
  {
     if (!document.getElementById(xinha_editors[i])) 
     {
       continue;
     }
     var main = document.getElementById("loading_" + document.getElementById(xinha_editors[i]).id);
     main.parentNode.removeChild(main);
  }
  Xinha.loadingMessages = null;
};

/** List of objects that have to be trated on page unload in order to work around the broken 
 * Garbage Collector in IE
 * @private
 * @see Xinha#freeLater
 * @see Xinha#free
 * @see Xinha#collectGarbageForIE
 */
Xinha.toFree = [];
/** Adds objects to Xinha.toFree 
 * @param {Object} object The object to free memory
 * @param (String} prop optional  The property to release
 * @private
 * @see Xinha#toFree
 * @see Xinha#free
 * @see Xinha#collectGarbageForIE
 */
Xinha.freeLater = function(obj,prop)
{
  Xinha.toFree.push({o:obj,p:prop});
};

/** Release memory properties from object
 * @param {Object} object The object to free memory
 * @param (String} prop optional The property to release
 * @private
 * @see Xinha#collectGarbageForIE
 * @see Xinha#free
 */
Xinha.free = function(obj, prop)
{
  if ( obj && !prop )
  {
    for ( var p in obj )
    {
      Xinha.free(obj, p);
    }
  }
  else if ( obj )
  {
    if ( prop.indexOf('src') == -1 ) // if src (also lowsrc, and maybe dynsrc ) is set to null, a file named "null" is requested from the server (see #1001)
    {
      try { obj[prop] = null; } catch(x) {}
    }
  }
};

/** IE's Garbage Collector is broken very badly.  We will do our best to 
 *   do it's job for it, but we can't be perfect. Takes all objects from Xinha.free and releases sets the null
 * @private
 * @see Xinha#toFree
 * @see Xinha#free
 */

Xinha.collectGarbageForIE = function() 
{  
  Xinha.flushEvents();   
  for ( var x = 0; x < Xinha.toFree.length; x++ )
  {
    Xinha.free(Xinha.toFree[x].o, Xinha.toFree[x].p);
    Xinha.toFree[x].o = null;
  }
};


// The following methods may be over-ridden or extended by the browser specific
// javascript files.


/** Insert a node at the current selection point. 
 * @param {DomNode} toBeInserted
 */

Xinha.prototype.insertNodeAtSelection = function(toBeInserted) { Xinha.notImplemented("insertNodeAtSelection"); };

/** Get the parent element of the supplied or current selection. 
 *  @param {Selection} sel optional selection as returned by getSelection
 *  @returns {DomNode}
 */
  
Xinha.prototype.getParentElement      = function(sel) { Xinha.notImplemented("getParentElement"); };

/**
 * Returns the selected element, if any.  That is,
 * the element that you have last selected in the "path"
 * at the bottom of the editor, or a "control" (eg image)
 *
 * @returns {DomNode|null}
 */
 
Xinha.prototype.activeElement         = function(sel) { Xinha.notImplemented("activeElement"); };

/** 
 * Determines if the given selection is empty (collapsed).
 * @param {Selection} sel Selection object as returned by getSelection
 * @returns {Boolean}
 */
 
Xinha.prototype.selectionEmpty        = function(sel) { Xinha.notImplemented("selectionEmpty"); };
/** 
 * Returns a range object to be stored 
 * and later restored with Xinha.prototype.restoreSelection()
 * @returns {Range}
 */

Xinha.prototype.saveSelection = function() { Xinha.notImplemented("saveSelection"); };

/** Restores a selection previously stored
 * @param {Range} savedSelection Range object as returned by Xinha.prototype.restoreSelection()
 */
Xinha.prototype.restoreSelection = function(savedSelection)  { Xinha.notImplemented("restoreSelection"); };

/**
 * Selects the contents of the given node.  If the node is a "control" type element, (image, form input, table)
 * the node itself is selected for manipulation.
 *
 * @param {DomNode} node 
 * @param {Integer} pos  Set to a numeric position inside the node to collapse the cursor here if possible. 
 */
Xinha.prototype.selectNodeContents    = function(node,pos) { Xinha.notImplemented("selectNodeContents"); };

/** Insert HTML at the current position, deleting the selection if any. 
 *  
 *  @param {String} html
 */
 
Xinha.prototype.insertHTML            = function(html) { Xinha.notImplemented("insertHTML"); };

/** Get the HTML of the current selection.  HTML returned has not been passed through outwardHTML.
 *
 * @returns {String}
 */
Xinha.prototype.getSelectedHTML       = function() { Xinha.notImplemented("getSelectedHTML"); };

/** Get a Selection object of the current selection.  Note that selection objects are browser specific.
 *
 * @returns {Selection}
 */
 
Xinha.prototype.getSelection          = function() { Xinha.notImplemented("getSelection"); };

/** Create a Range object from the given selection.  Note that range objects are browser specific.
 *  @see Xinha#getSelection
 *  @param {Selection} sel Selection object 
 *  @returns {Range}
 */
Xinha.prototype.createRange           = function(sel) { Xinha.notImplemented("createRange"); };

/** Determine if the given event object is a keydown/press event.
 *
 *  @param {Event} event 
 *  @returns {Boolean}
 */
 
Xinha.prototype.isKeyEvent            = function(event) { Xinha.notImplemented("isKeyEvent"); };

/** Determines if the given key event object represents a combination of CTRL-<key>,
 *  which for Xinha is a shortcut.  Note that CTRL-ALT-<key> is not a shortcut.
 *
 *  @param    {Event} keyEvent
 *  @returns  {Boolean}
 */
 
Xinha.prototype.isShortCut = function(keyEvent)
{
  if(keyEvent.ctrlKey && !keyEvent.altKey)
  {
    return true;
  }
  
  return false;
};

/** Return the character (as a string) of a keyEvent  - ie, press the 'a' key and
 *  this method will return 'a', press SHIFT-a and it will return 'A'.
 * 
 *  @param   {Event} keyEvent
 *  @returns {String}
 */
                                   
Xinha.prototype.getKey = function(keyEvent) { Xinha.notImplemented("getKey"); };

/** Return the HTML string of the given Element, including the Element.
 * 
 * @param {DomNode} element HTML Element
 * @returns {String}
 */
 
Xinha.getOuterHTML = function(element) { Xinha.notImplemented("getOuterHTML"); };

/** Get a new XMLHTTPRequest Object ready to be used. 
 *
 * @returns {XMLHTTPRequest}
 */

Xinha.getXMLHTTPRequestObject = function() 
{
  try
  {    
    if (typeof XMLHttpRequest != "undefined" && typeof XMLHttpRequest.constructor == 'function' ) // Safari's XMLHttpRequest is typeof object
    {
  	  return new XMLHttpRequest();
    }
  	else if (typeof ActiveXObject == "function")
  	{
  	  return new ActiveXObject("Microsoft.XMLHTTP");
  	}
  }
  catch(e)
  {
    Xinha.notImplemented('getXMLHTTPRequestObject');
  }
};
 
// Compatability - all these names are deprecated and will be removed in a future version
/** Alias of activeElement()
 * @see Xinha#activeElement
 * @deprecated
 * @returns {DomNode|null}
 */
Xinha.prototype._activeElement  = function(sel) { return this.activeElement(sel); };
/** Alias of selectionEmpty()
 * @see Xinha#selectionEmpty
 * @deprecated
 * @param {Selection} sel Selection object as returned by getSelection
 * @returns {Boolean}
 */
Xinha.prototype._selectionEmpty = function(sel) { return this.selectionEmpty(sel); };
/** Alias of getSelection()
 * @see Xinha#getSelection
 * @deprecated
 * @returns {Selection}
 */
Xinha.prototype._getSelection   = function() { return this.getSelection(); };
/** Alias of createRange()
 * @see Xinha#createRange
 * @deprecated
 * @param {Selection} sel Selection object
 * @returns {Range}
 */
Xinha.prototype._createRange    = function(sel) { return this.createRange(sel); };
HTMLArea = Xinha;

//what is this for? Do we need it?
Xinha.init();

if ( Xinha.ie_version < 8 )
{
  Xinha.addDom0Event(window,'unload',Xinha.collectGarbageForIE);
}
/** Print some message to Firebug, Webkit, Opera, or IE8 console
 * 
 * @param {String} text
 * @param {String} level one of 'warn', 'info', or empty 
 */
Xinha.debugMsg = function(text, level)
{
  if (typeof console != 'undefined' && typeof console.log == 'function') 
  {
    if (level && level == 'warn' && typeof console.warn == 'function') 
    {
      console.warn(text);
    }
    else 
      if (level && level == 'info' && typeof console.info == 'function') 
      {
        console.info(text);
      }
      else 
      {
        console.log(text);
      }
  }
  else if (typeof opera != 'undefined' && typeof opera.postError == 'function') 
  {
    opera.postError(text);
  }
};
Xinha.notImplemented = function(methodName) 
{
  throw new Error("Method Not Implemented", "Part of Xinha has tried to call the " + methodName + " method which has not been implemented.");
};
